diff --git a/components/dashboard/ContributionForecast.test.tsx b/components/dashboard/ContributionForecast.test.tsx new file mode 100644 index 000000000..074a86efa --- /dev/null +++ b/components/dashboard/ContributionForecast.test.tsx @@ -0,0 +1,117 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import ContributionForecast from './ContributionForecast'; +import type { ActivityData } from '@/types/dashboard'; + +// Mock framer-motion to prevent animation issues during testing +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className, whileHover, ...props }: any) => ( +
+ {children} +
+ ), + }, +})); + +// Mock lucide-react +vi.mock('lucide-react', () => ({ + TrendingUp: () =>
, + TrendingDown: () =>
, + Minus: () =>
, + Sparkles: () =>
, + Calendar: () =>
, + Zap: () =>
, + LineChart: () =>
, + Activity: () =>
, + Target: () =>
, +})); + +describe('ContributionForecast', () => { + // Helper for generating sequential active days + const generateMockActivity = (counts: number[], startYear = 2026): ActivityData[] => { + return counts.map((count, index) => { + // pad with leading zero + const dayStr = String(index + 1).padStart(2, '0'); + return { + date: `${startYear}-06-${dayStr}`, + count, + intensity: count > 0 ? 3 : 0, + }; + }); + }; + + it('renders forecast headers and elements', () => { + const activity = generateMockActivity([1, 2, 3]); + render(); + + expect(screen.getByRole('heading', { name: /Contribution Forecast/i })).toBeDefined(); + expect(screen.getByText(/Predict future growth/i)).toBeDefined(); + }); + + it('calculates average weekly and monthly velocities correctly', () => { + // 7 days, 2 commits each day + // total commits = 14. avg daily = 2. weekly = 14. monthly = 60. + const activity = generateMockActivity([2, 2, 2, 2, 2, 2, 2]); + render(); + + expect(screen.getByText('14.0 Commits/Week')).toBeDefined(); + expect(screen.getByText('60.0 Commits/Month')).toBeDefined(); + }); + + it('handles empty activity data gracefully', () => { + render(); + + expect(screen.getByText(/No past activity data available to generate predictions/i)).toBeDefined(); + expect(screen.queryByText('commits/wk')).toBeNull(); + }); + + it('handles zero-commit activity history correctly', () => { + const activity = generateMockActivity([0, 0, 0, 0, 0]); + render(); + + expect(screen.getByText('0.0 Commits/Week')).toBeDefined(); + expect(screen.getByText('0.0 Commits/Month')).toBeDefined(); + expect(screen.getByText('Inactive')).toBeDefined(); + expect(screen.getByText('Stable Rhythm')).toBeDefined(); + + // Projections should equal currentTotal contributions (100) because slope and intercept are 0 + const projectedCommits = screen.getAllByText('100 Commits'); + expect(projectedCommits.length).toBeGreaterThanOrEqual(2); + }); + + it('correctly projects month-end and year-end contributions with linear regression', () => { + // Let's create an increasing activity + // counts: 1 to 10. last entry: 2026-06-10. + // N = 10, meanX = 4.5, meanY = 5.5, slope = 1, intercept = 1 + // End of June (2026-06-30). Days remaining: 20 days. + // Projected daily commits for days 11 to 30: + // y_t = t + 1 (since slope=1, intercept=1). + // For t = 10 to 29 (day index, where day 1 is index 0, day 30 is index 29): + // index for future day is N - 1 + d = 9 + d. + // y_t = (9 + d) * 1 + 1 = 10 + d. + // For d = 1 to 20: 11 + 12 + ... + 30 = 410. + // Current total in activity = 55. + // Projected month end total = 55 + 410 = 465. + const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const activity = generateMockActivity(counts); + + render(); + + // Since totalContributions is not specified, it falls back to sum of activity counts (55). + // Month-End target should show 465 commits. + expect(screen.getByText('465 Commits')).toBeDefined(); + }); + + it('determines consistency rating and trend categories correctly', () => { + // 10 days, 9 active days -> active ratio = 90% -> Elite consistency + const activity = generateMockActivity([1, 2, 3, 0, 4, 5, 6, 7, 8, 9]); + render(); + + expect(screen.getByText('Elite (Very Consistent)')).toBeDefined(); + expect(screen.getByText('90% active days')).toBeDefined(); + + // slope is positive and strong + expect(screen.getByText('Strong Growth')).toBeDefined(); + }); +}); diff --git a/components/dashboard/ContributionForecast.tsx b/components/dashboard/ContributionForecast.tsx new file mode 100644 index 000000000..077e96d13 --- /dev/null +++ b/components/dashboard/ContributionForecast.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { + TrendingUp, + TrendingDown, + Minus, + Sparkles, + Calendar, + Zap, + LineChart, + Activity, + Target +} from 'lucide-react'; +import type { ActivityData } from '@/types/dashboard'; +import { useTranslation } from '@/context/TranslationContext'; + +interface ContributionForecastProps { + activity: ActivityData[]; + totalContributions?: number; +} + +export default function ContributionForecast({ activity = [], totalContributions }: ContributionForecastProps) { + const { t } = useTranslation(); + + const calculations = useMemo(() => { + if (!activity || activity.length === 0) { + return { + weeklyVelocity: 0, + monthlyVelocity: 0, + projectedMonthEndTotal: totalContributions || 0, + projectedYearEndTotal: totalContributions || 0, + consistencyScore: 0, + consistencyLevel: 'inactive', + slope: 0, + trendType: 'stable', + hasActivity: false, + }; + } + + const N = activity.length; + const currentTotal = totalContributions !== undefined ? totalContributions : activity.reduce((sum, d) => sum + d.count, 0); + + // 1. Average Daily, Weekly, Monthly Velocity + const totalActivityCommits = activity.reduce((sum, d) => sum + d.count, 0); + const avgDaily = totalActivityCommits / N; + const weeklyVelocity = avgDaily * 7; + const monthlyVelocity = avgDaily * 30; + + // 2. Linear Regression (slope m and intercept c) + // x_i = i, y_i = activity[i].count + const meanX = (N - 1) / 2; + const meanY = totalActivityCommits / N; + + let num = 0; + let den = 0; + for (let i = 0; i < N; i++) { + const xDiff = i - meanX; + num += xDiff * (activity[i].count - meanY); + den += xDiff * xDiff; + } + + const slope = den !== 0 ? num / den : 0; + const intercept = meanY - slope * meanX; + + // 3. Date Projections + // Use the date of the last entry in activity as "current date", fallback to new Date() + const lastEntryDateStr = activity[N - 1].date; + let currentDate = new Date(lastEntryDateStr); + if (isNaN(currentDate.getTime())) { + currentDate = new Date(); + } + + // End of Month + const endOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); + const timeDiffMonth = endOfMonth.getTime() - currentDate.getTime(); + const daysRemainingInMonth = Math.max(0, Math.ceil(timeDiffMonth / (1000 * 60 * 60 * 24))); + + // End of Year + const endOfYear = new Date(currentDate.getFullYear(), 11, 31); + const timeDiffYear = endOfYear.getTime() - currentDate.getTime(); + const daysRemainingInYear = Math.max(0, Math.ceil(timeDiffYear / (1000 * 60 * 60 * 24))); + + // Loop to sum predicted future daily commits (guaranteed non-negative) + let projectedMonthExtra = 0; + for (let d = 1; d <= daysRemainingInMonth; d++) { + // day index for future day is N - 1 + d + const projectedDaily = slope * (N - 1 + d) + intercept; + projectedMonthExtra += Math.max(0, projectedDaily); + } + const projectedMonthEndTotal = Math.round(currentTotal + projectedMonthExtra); + + let projectedYearExtra = 0; + for (let d = 1; d <= daysRemainingInYear; d++) { + const projectedDaily = slope * (N - 1 + d) + intercept; + projectedYearExtra += Math.max(0, projectedDaily); + } + const projectedYearEndTotal = Math.round(currentTotal + projectedYearExtra); + + // 4. Consistency Score + // Active days (days with count > 0) + const activeDays = activity.filter(d => d.count > 0).length; + const activeRatio = activeDays / N; + const consistencyScore = Math.min(100, Math.round(activeRatio * 100)); + + let consistencyLevel: 'elite' | 'consistent' | 'occasional' | 'sporadic' | 'inactive' = 'inactive'; + if (consistencyScore >= 85) { + consistencyLevel = 'elite'; + } else if (consistencyScore >= 60) { + consistencyLevel = 'consistent'; + } else if (consistencyScore >= 30) { + consistencyLevel = 'occasional'; + } else if (consistencyScore > 0) { + consistencyLevel = 'sporadic'; + } + + // 5. Trend slope categorization + let trendType: 'strong_growth' | 'moderate_growth' | 'stable' | 'cooling' | 'decline' = 'stable'; + if (slope > 0.02) { + trendType = 'strong_growth'; + } else if (slope > 0.005) { + trendType = 'moderate_growth'; + } else if (slope < -0.02) { + trendType = 'decline'; + } else if (slope < -0.005) { + trendType = 'cooling'; + } + + return { + weeklyVelocity, + monthlyVelocity, + projectedMonthEndTotal, + projectedYearEndTotal, + consistencyScore, + consistencyLevel, + slope, + trendType, + hasActivity: true, + }; + }, [activity, totalContributions]); + + const { + weeklyVelocity, + monthlyVelocity, + projectedMonthEndTotal, + projectedYearEndTotal, + consistencyScore, + consistencyLevel, + slope, + trendType, + hasActivity, + } = calculations; + + const getTrendIcon = () => { + switch (trendType) { + case 'strong_growth': + case 'moderate_growth': + return ; + case 'decline': + case 'cooling': + return ; + default: + return ; + } + }; + + const getTrendColorClass = () => { + switch (trendType) { + case 'strong_growth': + case 'moderate_growth': + return 'text-emerald-600 dark:text-emerald-400'; + case 'decline': + case 'cooling': + return 'text-rose-600 dark:text-rose-400'; + default: + return 'text-zinc-500 dark:text-[#A1A1AA]'; + } + }; + + return ( + +
+
+ +
+

+ {t('dashboard.forecast.title')} +

+

+ {t('dashboard.forecast.subtitle')} +

+
+
+
+ + {!hasActivity ? ( +
+ +

{t('dashboard.forecast.no_activity')}

+
+ ) : ( + <> + {/* Main Metrics Grid */} +
+ +
+ {t('dashboard.forecast.weekly_velocity')} + +
+
+ {t('dashboard.forecast.commits_per_week', { count: weeklyVelocity.toFixed(1) })} +
+
+ + +
+ {t('dashboard.forecast.monthly_velocity')} + +
+
+ {t('dashboard.forecast.commits_per_month', { count: monthlyVelocity.toFixed(1) })} +
+
+ + +
+ {t('dashboard.forecast.projected_month')} + +
+
+ {t('dashboard.forecast.commits', { count: String(projectedMonthEndTotal) })} +
+
+ + +
+ {t('dashboard.forecast.projected_year')} + +
+
+ {t('dashboard.forecast.commits', { count: String(projectedYearEndTotal) })} +
+
+
+ + {/* Details Section */} +
+ {/* Consistency card */} +
+
+ + {t('dashboard.forecast.consistency_rating')} + + + {t(`dashboard.forecast.consistency_level.${consistencyLevel}`)} + +
+
+ +
+
+ {consistencyScore}% active days +
+
+ + {/* Trend card */} +
+
+ + {t('dashboard.forecast.trend_direction')} + + + {t(`dashboard.forecast.trends.${trendType}`)} + + + {t('dashboard.forecast.growth_rate', { rate: slope.toFixed(4) })} + +
+
+ {getTrendIcon()} +
+
+
+ + )} +
+ ); +} diff --git a/components/dashboard/ContributionReplay.test.tsx b/components/dashboard/ContributionReplay.test.tsx new file mode 100644 index 000000000..98f78e52d --- /dev/null +++ b/components/dashboard/ContributionReplay.test.tsx @@ -0,0 +1,207 @@ +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import ContributionReplay from './ContributionReplay'; + +// Mock framer-motion to avoid animation issues in tests +vi.mock('framer-motion', () => ({ + motion: { + div: ({ + children, + className, + ...props + }: { + children: React.ReactNode; + className?: string; + [key: string]: unknown; + }) => { + const safeProps = { ...props }; + delete safeProps.initial; + delete safeProps.whileInView; + delete safeProps.viewport; + delete safeProps.transition; + return ( +
+ {children} +
+ ); + }, + span: ({ children, ...props }: any) => {children}, + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock lucide-react +vi.mock('lucide-react', () => ({ + Play: (props: any) =>
, + Pause: (props: any) =>
, + RotateCcw: (props: any) =>
, + Calendar: (props: any) =>
, + Flame: (props: any) =>
, +})); + +const mockActivity = [ + { date: '2025-01-05', count: 5, intensity: 1 as const }, + { date: '2025-01-10', count: 10, intensity: 4 as const }, // Peak day + { date: '2025-02-15', count: 3, intensity: 2 as const }, + { date: '2025-03-20', count: 8, intensity: 3 as const }, +]; + +describe('ContributionReplay', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + it('renders the header title and description', () => { + render(); + + expect(screen.getByText('Contribution Replay Timeline')).toBeDefined(); + expect(screen.getByText(/Animate your coding journey/)).toBeDefined(); + }); + + it('renders the empty state fallback when no activity data is provided', () => { + render(); + + expect(screen.getByText('No contribution activity data available for replay.')).toBeDefined(); + }); + + it('toggles play/pause state and correctly accumulates commits on tick', () => { + render(); + + // Initial State: Month = January 2025, Commits = 15, Accumulated = 15 + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + + // Click play button + const playBtn = screen.getByLabelText('Play Replay'); + fireEvent.click(playBtn); + + // Play changes to Pause button + expect(screen.getByLabelText('Pause Replay')).toBeDefined(); + + // Tick 1 (1000ms): February 2025 (Monthly commits = 3, Accumulated commits = 18) + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + expect(screen.getAllByText('3')[0]).toBeDefined(); // Monthly Commits + expect(screen.getByText('18')).toBeDefined(); // Accumulated Commits + + // Tick 2 (1000ms): March 2025 (Monthly commits = 8, Accumulated commits = 26) + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('March 2025')[0]).toBeDefined(); + expect(screen.getAllByText('8')[0]).toBeDefined(); + expect(screen.getByText('26')).toBeDefined(); + + // Loop back: Tick 3 (1000ms) loops back to January 2025 + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + + // Pause the replay + const pauseBtn = screen.getByLabelText('Pause Replay'); + fireEvent.click(pauseBtn); + + // Advancing timers should not change active month when paused + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + }); + + it('resets playback index back to 0 on reset click', () => { + render(); + + // Move forward + const playBtn = screen.getByLabelText('Play Replay'); + fireEvent.click(playBtn); + expect(screen.getByLabelText('Pause Replay')).toBeDefined(); + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + + // Reset replay + const resetBtn = screen.getByLabelText('Reset Replay'); + fireEvent.click(resetBtn); + + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + // Timer should also be stopped + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + }); + + it('updates the month on timeline manual scrub slider input change', () => { + render(); + + const scrubSlider = screen.getByLabelText('Scrub timeline months') as HTMLInputElement; + + // Move slider to index 2 (March 2025) + fireEvent.change(scrubSlider, { target: { value: '2' } }); + + expect(screen.getAllByText('March 2025')[0]).toBeDefined(); + expect(screen.getAllByText('8')[0]).toBeDefined(); // Monthly Commits + expect(screen.getByText('26')).toBeDefined(); // Accumulated Commits + }); + + it('updates the month on speed index and speed multiplier change', () => { + render(); + + // Verify default speed label + expect(screen.getByText('1x')).toBeDefined(); + + // Change speed via speed slider to 2x (index 1) + const speedSlider = screen.getByLabelText('Select playback speed multiplier') as HTMLInputElement; + fireEvent.change(speedSlider, { target: { value: '1' } }); + + expect(screen.getByText('2x')).toBeDefined(); + + // Click play and verify tick speed is now 500ms + const playBtn = screen.getByLabelText('Play Replay'); + fireEvent.click(playBtn); + expect(screen.getByLabelText('Pause Replay')).toBeDefined(); + + // Advance by 550ms -> should tick to February 2025 + act(() => { + vi.advanceTimersByTime(550); + }); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + }); + + it('supports keyboard accessibility for controls and navigation', () => { + render(); + + // Controls should have tabIndex 0 and be natively focusable + const playBtn = screen.getByLabelText('Play Replay'); + playBtn.focus(); + expect(document.activeElement).toBe(playBtn); + + const resetBtn = screen.getByLabelText('Reset Replay'); + resetBtn.focus(); + expect(document.activeElement).toBe(resetBtn); + + // Month Navigation list buttons + const monthBtns = screen.getAllByLabelText(/Jump to/); + expect(monthBtns.length).toBeGreaterThan(0); + + // Focus first month jump button + monthBtns[1].focus(); + expect(document.activeElement).toBe(monthBtns[1]); + + // Keyboard click triggering month jump + fireEvent.click(monthBtns[1]); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + }); +}); diff --git a/components/dashboard/ContributionReplay.tsx b/components/dashboard/ContributionReplay.tsx new file mode 100644 index 000000000..d315ce05e --- /dev/null +++ b/components/dashboard/ContributionReplay.tsx @@ -0,0 +1,465 @@ +'use client'; + +import { useState, useEffect, useRef, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { Play, Pause, RotateCcw, Calendar, Flame } from 'lucide-react'; +import { useTranslation } from '@/context/TranslationContext'; +import type { ActivityData } from '@/types/dashboard'; + +interface MonthData { + monthKey: string; // "YYYY-MM" + label: string; // short formatted e.g. "Jan '25" + fullLabel: string; // full formatted e.g. "January 2025" + commits: number; + activityItems: ActivityData[]; +} + +export interface ContributionReplayProps { + activity?: ActivityData[]; +} + +export default function ContributionReplay({ activity = [] }: ContributionReplayProps) { + const { t } = useTranslation(); + + // Controls state + const [isPlaying, setIsPlaying] = useState(false); + const [currentMonthIndex, setCurrentMonthIndex] = useState(0); + const [playbackSpeed, setPlaybackSpeed] = useState(1); // 1, 2, 4 + const timerRef = useRef(null); + + // Chronologically sort and process activity data safely + const processedData = useMemo(() => { + const activityList = activity || []; + const sorted = [...activityList].sort((a, b) => a.date.localeCompare(b.date)); + + // Group by month YYYY-MM + const monthsMap = new Map(); + let peakDay = { date: '', count: 0 }; + + sorted.forEach((item) => { + if (!item.date) return; + const key = item.date.substring(0, 7); // "YYYY-MM" + if (!monthsMap.has(key)) { + monthsMap.set(key, { commits: 0, items: [] }); + } + + const group = monthsMap.get(key)!; + group.commits += item.count || 0; + group.items.push(item); + + if ((item.count || 0) > peakDay.count) { + peakDay = { date: item.date, count: item.count }; + } + }); + + const monthsList: MonthData[] = Array.from(monthsMap.entries()).map(([monthKey, data]) => { + let label = monthKey; + let fullLabel = monthKey; + + const parts = monthKey.split('-'); + if (parts.length === 2) { + const year = parts[0]; + const monthNum = parseInt(parts[1], 10); + const monthNames = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + const fullMonthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + const idx = monthNum - 1; + if (idx >= 0 && idx < 12) { + label = `${monthNames[idx]} '${year.substring(2)}`; + fullLabel = `${fullMonthNames[idx]} ${year}`; + } + } + + return { + monthKey, + label, + fullLabel, + commits: data.commits, + activityItems: data.items, + }; + }).sort((a, b) => a.monthKey.localeCompare(b.monthKey)); + + // Calculate cumulative sums + const accumulatedCommits: number[] = []; + let sum = 0; + monthsList.forEach((m) => { + sum += m.commits; + accumulatedCommits.push(sum); + }); + + const maxMonthCommits = monthsList.reduce((max, m) => Math.max(max, m.commits), 0) || 1; + + return { + monthsList, + accumulatedCommits, + peakDay, + maxMonthCommits, + }; + }, [activity]); + + const { monthsList, accumulatedCommits, peakDay, maxMonthCommits } = processedData; + + // Handle auto-playback ticks + useEffect(() => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + + if (isPlaying && monthsList.length > 0) { + const intervalTime = Math.max(100, 1000 / playbackSpeed); + timerRef.current = setInterval(() => { + setCurrentMonthIndex((prevIndex) => { + if (prevIndex >= monthsList.length - 1) { + return 0; // Loop back + } + return prevIndex + 1; + }); + }, intervalTime); + } + + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, [isPlaying, playbackSpeed, monthsList.length]); + + // Clean up playback on component unmount + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + + const activeMonth = monthsList[currentMonthIndex] || monthsList[0]; + const accumulatedSoFar = accumulatedCommits[currentMonthIndex] ?? 0; + + // Active month statistics breakdown (low, medium, high intensity) + const activeMonthBreakdown = useMemo(() => { + let low = 0; + let medium = 0; + let high = 0; + if (!activeMonth || !activeMonth.activityItems) { + return { low, medium, high }; + } + activeMonth.activityItems.forEach((d) => { + if (d.count > 0) { + if (d.intensity <= 1) { + low++; + } else if (d.intensity <= 3) { + medium++; + } else { + high++; + } + } + }); + return { low, medium, high }; + }, [activeMonth]); + + // Empty state boundary check + if (monthsList.length === 0) { + return ( +
+ +

+ {t('dashboard.replay.title')} +

+

+ {t('dashboard.replay.no_activity')} +

+
+ ); + } + + const speedOptions = [1, 2, 4]; + const speedIndex = speedOptions.indexOf(playbackSpeed); + + const handleReset = () => { + setIsPlaying(false); + setCurrentMonthIndex(0); + }; + + const togglePlay = () => { + setIsPlaying(!isPlaying); + }; + + return ( + + {/* Header */} +
+
+

+ + {t('dashboard.replay.title')} +

+

+ {t('dashboard.replay.subtitle')} +

+
+ + {/* Peak Activity Indicator */} + {peakDay.count > 0 && ( +
+ + {t('dashboard.replay.peak_activity')}: + + {t('dashboard.replay.peak_commits', { count: peakDay.count.toString() })}{' '} + {t('dashboard.replay.peak_date', { date: peakDay.date })} + +
+ )} +
+ + {/* Main stats visual dashboard */} +
+
+

+ {t('dashboard.replay.active_month')} +

+

+ {activeMonth.fullLabel} +

+
+ +
+

+ {t('dashboard.replay.monthly_total')} +

+

+ {activeMonth.commits} +

+
+ +
+

+ {t('dashboard.replay.accumulated')} +

+

+ {accumulatedSoFar} +

+
+
+ + {/* Interactive Visualizations */} +
+ {/* Month Calendar Mini Grid */} +
+
+

+ {activeMonth.fullLabel} — Daily Rhythm +

+ + {activeMonth.activityItems.length === 0 ? ( +

+ {t('dashboard.replay.no_activity')} +

+ ) : ( +
+ {activeMonth.activityItems.map((day, idx) => { + let colorClass = 'bg-neutral-100 dark:bg-neutral-800/50 border border-transparent'; + if (day.count > 0) { + if (day.intensity <= 1) { + colorClass = 'bg-indigo-500/20 border border-indigo-500/10 text-indigo-400'; + } else if (day.intensity <= 3) { + colorClass = 'bg-indigo-500/50 border border-indigo-500/20 text-indigo-300'; + } else { + colorClass = 'bg-indigo-500 border border-indigo-400 text-white shadow-[0_0_8px_rgba(99,102,241,0.4)]'; + } + } + + return ( +
+ {day.count > 0 ? day.count : ''} +
+ ); + })} +
+ )} +
+ + {/* Intensity counts breakdown */} +
+
+ + {t('dashboard.replay.intensity_low')}: {activeMonthBreakdown.low} +
+
+ + {t('dashboard.replay.intensity_medium')}: {activeMonthBreakdown.medium} +
+
+ + {t('dashboard.replay.intensity_high')}: {activeMonthBreakdown.high} +
+
+
+ + {/* Monthly Histogram Chart */} +
+
+

+ {t('dashboard.replay.monthly_total')} Breakdown +

+ +
+ {monthsList.map((m, idx) => { + const heightPercentage = Math.max(10, Math.min(100, (m.commits / maxMonthCommits) * 100)); + const isActive = idx === currentMonthIndex; + const isPassed = idx <= currentMonthIndex; + + return ( + + ); + })} +
+
+ +
+ {monthsList[0]?.label} + {monthsList[monthsList.length - 1]?.label} +
+
+
+ + {/* Playback Controls & Timeline Slider */} +
+ {/* Manual Timeline Scrub Slider */} +
+
+ {activeMonth.fullLabel} + {currentMonthIndex + 1} / {monthsList.length} +
+ setCurrentMonthIndex(parseInt(e.target.value, 10))} + aria-label={t('dashboard.replay.aria_scrub')} + className="w-full accent-indigo-500 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none h-2 cursor-pointer focus-visible:ring-2 focus-visible:ring-indigo-500 outline-none" + /> +
+ + {/* Buttons and Speed controls panel */} +
+
+ + + +
+ + {/* Speed Multiplier controls */} +
+ + { + const idx = parseInt(e.target.value, 10); + setPlaybackSpeed(speedOptions[idx]); + }} + aria-labelledby="speed-label" + aria-label={t('dashboard.replay.aria_speed')} + className="w-24 accent-indigo-500 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none h-1.5 cursor-pointer focus-visible:ring-2 focus-visible:ring-indigo-500 outline-none" + /> + + {t('dashboard.replay.speed_value', { speed: playbackSpeed.toString() })} + +
+
+
+ + {/* Horizontal Month Navigation Bar */} +
+ {monthsList.map((m, idx) => { + const isActive = idx === currentMonthIndex; + return ( + + ); + })} +
+
+ ); +} diff --git a/components/dashboard/DashboardClient.tsx b/components/dashboard/DashboardClient.tsx index 8852f9fcc..6a90bb22b 100644 --- a/components/dashboard/DashboardClient.tsx +++ b/components/dashboard/DashboardClient.tsx @@ -41,6 +41,10 @@ import PRInsightsClient from './PRInsights/PRInsightsClient'; import CIAnalyticsClient from './CIAnalytics/CIAnalyticsClient'; import DeploymentTracker from './DeploymentTracker'; import ArchitectureVisualizer from './ArchitectureVisualizer'; +import RepositoryImpactAnalyzer from './RepositoryImpactAnalyzer'; +import ContributionForecast from './ContributionForecast'; +import ProfileComparisonAnalytics from './ProfileComparisonAnalytics'; +import ContributionReplay from './ContributionReplay'; // Define the dashboard data structure export interface DashboardData { @@ -348,6 +352,42 @@ export default function DashboardClient({ const compareInputRef = useRef(null); const triggerRef = useRef(null); + const allRepos = [ + ...(initialData.popularRepos || []), + ...(initialData.pinnedRepos || []), + ...(initialData.starredRepos || []), + ]; + const deduplicatedRepos = Array.from( + new Map(allRepos.map((repo) => [repo.name, repo])).values() + ); + + const compareUser1 = { + profile: initialData.profile, + stats: initialData.stats, + languages: initialData.languages, + activity: initialData.activity, + achievements: initialData.achievements, + popularRepos: deduplicatedRepos, + }; + + const compareUser2 = secondUserData ? { + profile: secondUserData.profile, + stats: secondUserData.stats, + languages: secondUserData.languages, + activity: secondUserData.activity, + achievements: secondUserData.achievements, + popularRepos: [ + ...(secondUserData.popularRepos || []), + ...(secondUserData.pinnedRepos || []), + ...(secondUserData.starredRepos || []), + ].reduce((acc: Repository[], repo) => { + if (!acc.some((r) => r.name === repo.name)) { + acc.push(repo); + } + return acc; + }, []), + } : null; + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -722,6 +762,14 @@ export default function DashboardClient({ period={period} /> + +
+ +
+ +
+ +
+ +

Head-to-Head Stats diff --git a/components/dashboard/ProfileComparisonAnalytics.test.tsx b/components/dashboard/ProfileComparisonAnalytics.test.tsx new file mode 100644 index 000000000..dbe092c61 --- /dev/null +++ b/components/dashboard/ProfileComparisonAnalytics.test.tsx @@ -0,0 +1,257 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import ProfileComparisonAnalytics, { CompareUser } from './ProfileComparisonAnalytics'; + +// Mock TranslationContext +vi.mock('@/context/TranslationContext', () => ({ + useTranslation: () => ({ + t: (key: string, options?: any) => { + if (options && options.count !== undefined) { + return key; // Simple mock returning key + } + return key; + } + }) +})); + +// Mock framer-motion to render clean HTML containers for testing +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className, style, ...props }: any) => { + const { initial, animate, whileInView, viewport, transition, ...rest } = props; + return ( +
+ {children} +
+ ); + }, + }, +})); + +// Mock lucide-react icons +vi.mock('lucide-react', () => ({ + Award: () => , + Trophy: () => , + Flame: () => , + Sparkles: () => , + Star: () => , + GitFork: () => , + GitCommit: () => , + MapPin: () => , + Calendar: () => , + Check: () => , + Lock: () => , + ExternalLink: () => , + ChevronDown: () => , + ChevronUp: () => , +})); + +const mockUser1: CompareUser = { + profile: { + username: 'alice', + name: 'Alice Cooper', + avatarUrl: 'https://avatars.githubusercontent.com/u/1', + isPro: true, + bio: 'Frontend enthusiast', + location: 'New York', + joinedDate: 'Joined Oct 2020', + developerScore: 85, + stats: { repositories: 12, followers: 150, following: 80, stars: 300 } + }, + stats: { + currentStreak: 5, + peakStreak: 15, + totalContributions: 500 + }, + languages: [ + { name: 'TypeScript', color: '#3178c6', percentage: 70 }, + { name: 'JavaScript', color: '#f1e05a', percentage: 30 } + ], + activity: [ + { date: '2026-05-10', count: 5, intensity: 2 }, + { date: '2026-05-11', count: 10, intensity: 3 }, + { date: '2026-06-01', count: 2, intensity: 1 } + ], + achievements: [ + { id: 'streak-10', title: '10 Day Streak', description: 'Coded for 10 days', icon: 'flame', isUnlocked: true, type: 'streak', threshold: 10, currentValue: 10, progress: 100 }, + { id: 'contrib-100', title: 'Centurion', description: '100 contributions', icon: 'trophy', isUnlocked: true, type: 'contributions', threshold: 100, currentValue: 100, progress: 100 }, + { id: 'ach-3', title: 'Achievement 3', description: 'Description 3', icon: 'star', isUnlocked: true, type: 'streak', threshold: 5, currentValue: 5, progress: 100 }, + { id: 'ach-4', title: 'Achievement 4', description: 'Description 4', icon: 'star', isUnlocked: true, type: 'streak', threshold: 5, currentValue: 5, progress: 100 }, + { id: 'ach-5', title: 'Achievement 5', description: 'Description 5', icon: 'star', isUnlocked: true, type: 'streak', threshold: 5, currentValue: 5, progress: 100 } + ], + popularRepos: [ + { name: 'alice-cool-web', description: 'Alice web project', stargazerCount: 50, forkCount: 10, url: 'https://github.com/alice/alice-cool-web', primaryLanguage: { name: 'TypeScript', color: '#3178c6' }, commits: 20 } as any, + { name: 'alice-utils', description: 'Alice util project', stargazerCount: 10, forkCount: 2, url: 'https://github.com/alice/alice-utils', primaryLanguage: { name: 'JavaScript', color: '#f1e05a' }, commits: 5 } as any + ] +}; + +const mockUser2: CompareUser = { + profile: { + username: 'bob', + name: 'Bob Builder', + avatarUrl: 'https://avatars.githubusercontent.com/u/2', + isPro: false, + bio: 'Backend architect', + location: 'Berlin', + joinedDate: 'Joined Jan 2021', + developerScore: 90, + stats: { repositories: 8, followers: 90, following: 40, stars: 120 } + }, + stats: { + currentStreak: 2, + peakStreak: 8, + totalContributions: 320 + }, + languages: [ + { name: 'TypeScript', color: '#3178c6', percentage: 40 }, + { name: 'Go', color: '#00add8', percentage: 60 } + ], + activity: [ + { date: '2026-05-10', count: 3, intensity: 1 }, + { date: '2026-06-01', count: 8, intensity: 3 } + ], + achievements: [ + { id: 'streak-10', title: '10 Day Streak', description: 'Coded for 10 days', icon: 'flame', isUnlocked: false, type: 'streak', threshold: 10, currentValue: 4, progress: 40 }, + { id: 'contrib-100', title: 'Centurion', description: '100 contributions', icon: 'trophy', isUnlocked: true, type: 'contributions', threshold: 100, currentValue: 100, progress: 100 } + ], + popularRepos: [ + { name: 'bob-go-engine', description: 'Bob high performance engine', stargazerCount: 100, forkCount: 30, url: 'https://github.com/bob/bob-go-engine', primaryLanguage: { name: 'Go', color: '#00add8' }, commits: 40 } as any, + { name: 'bob-script', description: 'Bob basic script', stargazerCount: 5, forkCount: 0, url: 'https://github.com/bob/bob-script', primaryLanguage: { name: 'Go', color: '#00add8' }, commits: 2 } as any + ] +}; + +describe('ProfileComparisonAnalytics', () => { + it('renders usernames, developer scores, and key stats in side-by-side layout', () => { + render(); + + // Check profiles + expect(screen.getByText('Alice Cooper')).toBeDefined(); + expect(screen.getAllByText('@alice')[0]).toBeDefined(); + expect(screen.getByText('Bob Builder')).toBeDefined(); + expect(screen.getAllByText('@bob')[0]).toBeDefined(); + + // Check developer score display + expect(screen.getByText('dashboard.compare.developer_score: 85')).toBeDefined(); + expect(screen.getByText('dashboard.compare.developer_score: 90')).toBeDefined(); + + // Check total contributions + expect(screen.getAllByText('dashboard.compare.contribution_comparison.user_total')[0]).toBeDefined(); + }); + + it('correctly ranks repositories and calculates impact scores', () => { + render(); + + // Scores check: + // bob-go-engine: 40 commits * 3 + 100 stars * 5 + 30 forks * 10 = 920 + // alice-cool-web: 20 commits * 3 + 50 stars * 5 + 10 forks * 10 = 410 + // alice-utils: 5 commits * 3 + 10 stars * 5 + 2 forks * 10 = 85 + // bob-script: 2 commits * 3 + 5 stars * 5 + 0 forks * 10 = 31 + + // Renders repository names + expect(screen.getByText('bob-go-engine')).toBeDefined(); + expect(screen.getByText('alice-cool-web')).toBeDefined(); + expect(screen.getByText('alice-utils')).toBeDefined(); + expect(screen.getByText('bob-script')).toBeDefined(); + + // Check impact score renders + expect(screen.getByText('920')).toBeDefined(); + expect(screen.getByText('410')).toBeDefined(); + expect(screen.getByText('85')).toBeDefined(); + expect(screen.getByText('31')).toBeDefined(); + }); + + it('displays language side-by-side percentage comparisons correctly', () => { + render(); + + // Languages: TS (Alice 70%, Bob 40%), JS (Alice 30%, Bob 0%), Go (Alice 0%, Bob 60%) + expect(screen.getByText('70.0%')).toBeDefined(); + expect(screen.getByText('40.0%')).toBeDefined(); + expect(screen.getByText('30.0%')).toBeDefined(); + expect(screen.getByText('60.0%')).toBeDefined(); + }); + + it('renders achievements status with unlock/lock badges correctly', () => { + render(); + + // We have "10 Day Streak" which is unlocked for Alice but locked for Bob + expect(screen.getByText('10 Day Streak')).toBeDefined(); + + // Alice unlocked badge, Bob locked badge + const aliceUnlockedBadges = screen.getAllByRole('img', { name: 'alice unlocked 10 Day Streak' }); + const bobLockedBadges = screen.getAllByRole('img', { name: 'bob locked 10 Day Streak' }); + + expect(aliceUnlockedBadges.length).toBe(1); + expect(bobLockedBadges.length).toBe(1); + }); + + it('handles zero-activity and empty-state edge cases gracefully', () => { + const emptyUser1: CompareUser = { + profile: { + username: 'empty1', + name: 'Empty One', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, + stats: { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, + languages: [], + activity: [], + achievements: [], + popularRepos: [] + }; + + const emptyUser2: CompareUser = { + profile: { + username: 'empty2', + name: 'Empty Two', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, + stats: { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, + languages: [], + activity: [], + achievements: [], + popularRepos: [] + }; + + render(); + + // Verify empty state fallback messages are rendered + expect(screen.getByText('dashboard.compare.contribution_comparison.no_data')).toBeDefined(); + expect(screen.getByText('dashboard.compare.language_comparison.no_data')).toBeDefined(); + expect(screen.getByText('dashboard.compare.achievement_comparison.no_data')).toBeDefined(); + expect(screen.getByText('dashboard.compare.repository_comparison.no_data')).toBeDefined(); + }); + + it('supports keyboard accessibility controls and ARIA compliance', () => { + const { container } = render(); + + // Region has comparison aria label + const region = screen.getByRole('region', { name: 'dashboard.compare.title' }); + expect(region).toBeDefined(); + + // Verify that links have accessible labels + const repoLinks = screen.getAllByRole('link'); + expect(repoLinks.length).toBeGreaterThanOrEqual(2); + expect(repoLinks[0].getAttribute('aria-label')).toContain('repository on GitHub'); + + // Achievement show more/less toggle button is keyboard-focusable + const toggleButton = screen.getByRole('button', { name: /dashboard.achievements.see_all/i }); + expect(toggleButton).toBeDefined(); + expect(toggleButton.tabIndex).toBe(0); + + // Click toggle button and check if it switches to 'show_less' + fireEvent.click(toggleButton); + expect(screen.getByText('dashboard.achievements.show_less')).toBeDefined(); + }); +}); diff --git a/components/dashboard/ProfileComparisonAnalytics.tsx b/components/dashboard/ProfileComparisonAnalytics.tsx new file mode 100644 index 000000000..897691342 --- /dev/null +++ b/components/dashboard/ProfileComparisonAnalytics.tsx @@ -0,0 +1,970 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { + Award, + Trophy, + Flame, + Sparkles, + Star, + GitFork, + GitCommit, + MapPin, + Calendar, + Check, + Lock, + ExternalLink, + ChevronDown, + ChevronUp +} from 'lucide-react'; +import { useTranslation } from '@/context/TranslationContext'; +import type { + UserProfile, + UserStats, + LanguageData, + ActivityData, + Achievement, + Repository +} from '@/types/dashboard'; + +// Types for the comparison component input +export interface CompareUser { + profile: UserProfile; + stats: UserStats; + languages: LanguageData[]; + activity: ActivityData[]; + achievements: Achievement[]; + popularRepos?: Repository[]; +} + +interface ProfileComparisonAnalyticsProps { + user1: CompareUser; + user2: CompareUser; +} + +export default function ProfileComparisonAnalytics({ + user1, + user2 +}: ProfileComparisonAnalyticsProps) { + const { t } = useTranslation(); + + // Safely fallback for User 1 + const u1Profile = useMemo(() => user1?.profile || { + username: 'user1', + name: 'User One', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, [user1]); + + const u1Stats = useMemo(() => user1?.stats || { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, [user1]); + const u1Languages = useMemo(() => user1?.languages || [], [user1]); + const u1Activity = useMemo(() => user1?.activity || [], [user1]); + const u1Achievements = useMemo(() => user1?.achievements || [], [user1]); + const u1Repos = useMemo(() => user1?.popularRepos || [], [user1]); + + // Safely fallback for User 2 + const u2Profile = useMemo(() => user2?.profile || { + username: 'user2', + name: 'User Two', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, [user2]); + + const u2Stats = useMemo(() => user2?.stats || { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, [user2]); + const u2Languages = useMemo(() => user2?.languages || [], [user2]); + const u2Activity = useMemo(() => user2?.activity || [], [user2]); + const u2Achievements = useMemo(() => user2?.achievements || [], [user2]); + const u2Repos = useMemo(() => user2?.popularRepos || [], [user2]); + + // State for toggling achievement lists + const [showAllAchievements, setShowAllAchievements] = useState(false); + + // 1. Process Activity month-by-month + const monthlyData = useMemo(() => { + const monthlyMap: Record = {}; + + const processAct = (act: ActivityData[], key: 'user1' | 'user2') => { + if (!Array.isArray(act)) return; + act.forEach((item) => { + if (!item.date) return; + const parts = item.date.split('-'); + if (parts.length < 2) return; + const year = parts[0]; + const month = parts[1]; + const monthYear = `${year}-${month}`; + if (!monthlyMap[monthYear]) { + monthlyMap[monthYear] = { user1: 0, user2: 0 }; + } + monthlyMap[monthYear][key] += item.count || 0; + }); + }; + + processAct(u1Activity, 'user1'); + processAct(u2Activity, 'user2'); + + const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + + return Object.entries(monthlyMap) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([monthYear, counts]) => { + const [year, monthStr] = monthYear.split('-'); + const monthIdx = parseInt(monthStr, 10) - 1; + const monthKey = monthNames[monthIdx] || monthStr; + const localizedMonthName = t(`dashboard.compare.months.${monthKey}`, { defaultValue: monthKey.toUpperCase() }); + return { + label: `${localizedMonthName} ${year}`, + ...counts + }; + }); + }, [u1Activity, u2Activity, t]); + + const maxMonthlyVal = useMemo(() => { + if (monthlyData.length === 0) return 1; + const maxVal = Math.max(...monthlyData.map((d) => Math.max(d.user1, d.user2)), 0); + return maxVal === 0 ? 1 : maxVal; + }, [monthlyData]); + + // 2. Process Languages side-by-side + const combinedLanguages = useMemo(() => { + const allLangs: Record = {}; + + u1Languages.forEach((l) => { + if (!l.name) return; + allLangs[l.name] = { + name: l.name, + color: l.color || '#94a3b8', + user1Pct: typeof l.percentage === 'number' && !isNaN(l.percentage) ? l.percentage : 0, + user2Pct: 0 + }; + }); + + u2Languages.forEach((l) => { + if (!l.name) return; + if (allLangs[l.name]) { + allLangs[l.name].user2Pct = typeof l.percentage === 'number' && !isNaN(l.percentage) ? l.percentage : 0; + } else { + allLangs[l.name] = { + name: l.name, + color: l.color || '#94a3b8', + user1Pct: 0, + user2Pct: typeof l.percentage === 'number' && !isNaN(l.percentage) ? l.percentage : 0 + }; + } + }); + + return Object.values(allLangs) + .sort((a, b) => (b.user1Pct + b.user2Pct) - (a.user1Pct + a.user2Pct)); + }, [u1Languages, u2Languages]); + + // 3. Process Achievements comparison + const mergedAchievements = useMemo(() => { + const map: Record< + string, + { + id: string; + title: string; + description: string; + type: Achievement['type']; + user1: Achievement | null; + user2: Achievement | null; + } + > = {}; + + u1Achievements.forEach((a) => { + if (!a.id) return; + map[a.id] = { + id: a.id, + title: a.title || 'Achievement', + description: a.description || '', + type: a.type || 'contributions', + user1: a, + user2: null + }; + }); + + u2Achievements.forEach((a) => { + if (!a.id) return; + if (map[a.id]) { + map[a.id].user2 = a; + } else { + map[a.id] = { + id: a.id, + title: a.title || 'Achievement', + description: a.description || '', + type: a.type || 'contributions', + user1: null, + user2: a + }; + } + }); + + return Object.values(map); + }, [u1Achievements, u2Achievements]); + + const visibleAchievements = showAllAchievements + ? mergedAchievements + : mergedAchievements.slice(0, 4); + + // 4. Process and Rank Top 3 Repositories + const processTopRepos = (repos: Repository[]) => { + if (!Array.isArray(repos)) return []; + return repos + .map((repo) => { + // Resolve keys + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const commits = (repo as any).commits ?? (repo as any).commitCount ?? 0; + const stars = repo.stargazerCount ?? (repo as any).stars ?? 0; + const forks = repo.forkCount ?? (repo as any).forks ?? 0; + + // Impact Score: (commits * 3) + (stars * 5) + (forks * 10) + const score = commits * 3 + stars * 5 + forks * 10; + + let langName = 'Unknown'; + let langColor = '#94a3b8'; + if (repo.primaryLanguage) { + langName = repo.primaryLanguage.name || 'Unknown'; + langColor = repo.primaryLanguage.color || '#94a3b8'; + } + + return { + name: repo.name || 'unnamed-repo', + description: repo.description || '', + commits, + stars, + forks, + score, + url: repo.url || '#', + language: { name: langName, color: langColor } + }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, 3); + }; + + const u1TopRepos = useMemo(() => processTopRepos(u1Repos), [u1Repos]); + const u2TopRepos = useMemo(() => processTopRepos(u2Repos), [u2Repos]); + + // Symmetrical layout requires rendering indices 0, 1, 2 head-to-head + const maxRepoRank = 3; + + return ( +
+ {/* Header section with profile cards & VS floating circle */} +
+ + {/* Floating VS middle divider */} + + + {/* User 1 profile card */} + + {u1Profile.isPro && ( +
+ {t('dashboard.profile.pro')} +
+ )} + +
+
+
+ {u1Profile.avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {u1Profile.name + ) : ( +
+ {u1Profile.username.substring(0, 2).toUpperCase()} +
+ )} +
+
+

+ {u1Profile.name || u1Profile.username} +

+

@{u1Profile.username}

+ +
+ + + {t('dashboard.compare.developer_score')}: {u1Profile.developerScore} + +
+
+
+ + {u1Profile.bio && ( +

+ {u1Profile.bio} +

+ )} + +
+ {u1Profile.location && ( + + {u1Profile.location} + + )} + {u1Profile.joinedDate && ( + + {u1Profile.joinedDate} + + )} +
+
+ +
+
+ + {u1Profile.stats?.repositories || 0} + + {t('dashboard.profile.repos')} +
+
+ + {u1Profile.stats?.followers || 0} + + {t('dashboard.profile.followers')} +
+
+ + {u1Stats.totalContributions || 0} + + {t('dashboard.stats.contributions')} +
+
+
+ + {/* User 2 profile card */} + + {u2Profile.isPro && ( +
+ {t('dashboard.profile.pro')} +
+ )} + +
+
+
+ {u2Profile.avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {u2Profile.name + ) : ( +
+ {u2Profile.username.substring(0, 2).toUpperCase()} +
+ )} +
+
+

+ {u2Profile.name || u2Profile.username} +

+

@{u2Profile.username}

+ +
+ + + {t('dashboard.compare.developer_score')}: {u2Profile.developerScore} + +
+
+
+ + {u2Profile.bio && ( +

+ {u2Profile.bio} +

+ )} + +
+ {u2Profile.location && ( + + {u2Profile.location} + + )} + {u2Profile.joinedDate && ( + + {u2Profile.joinedDate} + + )} +
+
+ +
+
+ + {u2Profile.stats?.repositories || 0} + + {t('dashboard.profile.repos')} +
+
+ + {u2Profile.stats?.followers || 0} + + {t('dashboard.profile.followers')} +
+
+ + {u2Stats.totalContributions || 0} + + {t('dashboard.stats.contributions')} +
+
+
+
+ + {/* Contribution Comparison Section */} + +
+

+ {t('dashboard.compare.contribution_comparison.title')} +

+

+ {t('dashboard.compare.contribution_comparison.subtitle')} +

+
+ + {monthlyData.length === 0 ? ( +
+ {t('dashboard.compare.contribution_comparison.no_data')} +
+ ) : ( +
+ {/* Visual Bar Chart */} +
+ {monthlyData.map((data, idx) => { + const u1Pct = (data.user1 / maxMonthlyVal) * 100; + const u2Pct = (data.user2 / maxMonthlyVal) * 100; + + return ( +
+ + {/* Hover Stats Tooltip */} +
+

{data.label}

+

@{u1Profile.username}: {data.user1}

+

@{u2Profile.username}: {data.user2}

+
+ + {/* The double bar */} +
+ {/* User 1 bar (emerald) */} +
+
+
+ + {/* User 2 bar (indigo) */} +
+
+
+
+ + {/* Month label */} + + {data.label.split(' ')[0]} + +
+ ); + })} +
+ + {/* Quick legend with totals */} +
+
+ + + {t('dashboard.compare.contribution_comparison.user_total', { + name: `@${u1Profile.username}`, + count: u1Stats.totalContributions.toLocaleString() + })} + + + + {t('dashboard.compare.contribution_comparison.user_total', { + name: `@${u2Profile.username}`, + count: u2Stats.totalContributions.toLocaleString() + })} + +
+
+
+ )} + + + {/* Language Breakdown Section */} + +
+

+ {t('dashboard.compare.language_comparison.title')} +

+

+ {t('dashboard.compare.language_comparison.subtitle')} +

+
+ + {combinedLanguages.length === 0 ? ( +
+ {t('dashboard.compare.language_comparison.no_data')} +
+ ) : ( +
+ {combinedLanguages.map((lang) => ( +
+ {/* User 1 language percentage */} +
+ + {lang.user1Pct.toFixed(1)}% + +
+
+
+
+ + {/* Center language badge */} +
+ + + {lang.name} + +
+ + {/* User 2 language percentage */} +
+
+
+
+ + {lang.user2Pct.toFixed(1)}% + +
+
+ ))} +
+ )} + + + {/* Achievement Comparison Section */} + +
+
+

+ {t('dashboard.compare.achievement_comparison.title')} +

+

+ {t('dashboard.compare.achievement_comparison.subtitle')} +

+
+
+ + {mergedAchievements.length === 0 ? ( +
+ {t('dashboard.compare.achievement_comparison.no_data')} +
+ ) : ( +
+
+ @{u1Profile.username} + {t('dashboard.achievements.title')} + @{u2Profile.username} +
+ +
+ {visibleAchievements.map((item) => { + const isUnlocked1 = item.user1?.isUnlocked || false; + const isUnlocked2 = item.user2?.isUnlocked || false; + + // Let's decide how to display who unlocked it + let unlockedBy = t('dashboard.compare.achievement_comparison.neither'); + if (isUnlocked1 && isUnlocked2) { + unlockedBy = t('dashboard.compare.achievement_comparison.both'); + } else if (isUnlocked1) { + unlockedBy = `@${u1Profile.username}`; + } else if (isUnlocked2) { + unlockedBy = `@${u2Profile.username}`; + } + + return ( +
+ {/* User 1 Badge */} +
+ {isUnlocked1 ? ( +
+ + {t('dashboard.compare.achievement_comparison.unlocked')} +
+ ) : ( +
+ + {t('dashboard.compare.achievement_comparison.locked')} +
+ )} +
+ + {/* Achievement Details */} +
+
+ {item.type === 'streak' ? ( + + ) : item.type === 'behavior' ? ( + + ) : ( + + )} +

+ {item.title} +

+
+

+ {item.description} +

+ + {t('dashboard.compare.achievement_comparison.unlocked_by')}: {unlockedBy} + +
+ + {/* User 2 Badge */} +
+ {isUnlocked2 ? ( +
+ + {t('dashboard.compare.achievement_comparison.unlocked')} +
+ ) : ( +
+ + {t('dashboard.compare.achievement_comparison.locked')} +
+ )} +
+
+ ); + })} +
+ + {mergedAchievements.length > 4 && ( + + )} +
+ )} +
+ + {/* Repository Comparison Section */} + +
+

+ {t('dashboard.compare.repository_comparison.title')} +

+

+ {t('dashboard.compare.repository_comparison.subtitle')} +

+
+ + {u1TopRepos.length === 0 && u2TopRepos.length === 0 ? ( +
+ {t('dashboard.compare.repository_comparison.no_data')} +
+ ) : ( +
+ {Array.from({ length: maxRepoRank }).map((_, rankIdx) => { + const repo1 = u1TopRepos[rankIdx]; + const repo2 = u2TopRepos[rankIdx]; + + // If neither has a repository at this rank, don't render it + if (!repo1 && !repo2) return null; + + // Highlighting winners for individual stats + const scoreWinner = repo1 && repo2 ? (repo1.score > repo2.score ? 'user1' : repo1.score < repo2.score ? 'user2' : 'tie') : null; + const commitsWinner = repo1 && repo2 ? (repo1.commits > repo2.commits ? 'user1' : repo1.commits < repo2.commits ? 'user2' : 'tie') : null; + const starsWinner = repo1 && repo2 ? (repo1.stars > repo2.stars ? 'user1' : repo1.stars < repo2.stars ? 'user2' : 'tie') : null; + const forksWinner = repo1 && repo2 ? (repo1.forks > repo2.forks ? 'user1' : repo1.forks < repo2.forks ? 'user2' : 'tie') : null; + + return ( +
+ {/* Repo 1 details (Left) */} +
+ {repo1 ? ( +
+
+

+ {repo1.name} +

+ {repo1.language.name !== 'Unknown' && ( + + + {repo1.language.name} + + )} + + + +
+ {repo1.description && ( +

+ {repo1.description} +

+ )} + +
+
+ {t('dashboard.compare.repository_comparison.commits')} + + {repo1.commits} + +
+
+ {t('dashboard.compare.repository_comparison.stars')} + + {repo1.stars} + +
+
+ {t('dashboard.compare.repository_comparison.forks')} + + {repo1.forks} + +
+
+ {t('dashboard.compare.repository_comparison.impact_score')} + + {repo1.score} + +
+
+
+ ) : ( +
+ N/A +
+ )} +
+ + {/* Central Rank Indicator */} +
+ + {t('dashboard.compare.repository_comparison.rank')} + + + #{rankIdx + 1} + +
+ + {/* Repo 2 details (Right) */} +
+ {repo2 ? ( +
+
+ + + + {repo2.language.name !== 'Unknown' && ( + + + {repo2.language.name} + + )} +

+ {repo2.name} +

+
+ {repo2.description && ( +

+ {repo2.description} +

+ )} + +
+
+ {t('dashboard.compare.repository_comparison.commits')} + + {repo2.commits} + +
+
+ {t('dashboard.compare.repository_comparison.stars')} + + {repo2.stars} + +
+
+ {t('dashboard.compare.repository_comparison.forks')} + + {repo2.forks} + +
+
+ {t('dashboard.compare.repository_comparison.impact_score')} + + {repo2.score} + +
+
+
+ ) : ( +
+ N/A +
+ )} +
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/components/dashboard/RepositoryImpactAnalyzer.test.tsx b/components/dashboard/RepositoryImpactAnalyzer.test.tsx new file mode 100644 index 000000000..4c1459db5 --- /dev/null +++ b/components/dashboard/RepositoryImpactAnalyzer.test.tsx @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import RepositoryImpactAnalyzer, { formatAge } from './RepositoryImpactAnalyzer'; + +// Mock framer-motion to render clean HTML containers for testing +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className, style, ...props }: any) => { + delete props.initial; + delete props.animate; + delete props.whileInView; + delete props.viewport; + delete props.transition; + return ( +
+ {children} +
+ ); + }, + }, +})); + +// Helper to create dates relative to today +function getRelativeDateString(monthsAgo: number): string { + const d = new Date(); + d.setMonth(d.getMonth() - monthsAgo); + return d.toISOString(); +} + +describe('RepositoryImpactAnalyzer', () => { + it('renders an empty state when no repositories are provided', () => { + render(); + + expect(screen.getByText('Repository Impact Analyzer')).toBeDefined(); + expect(screen.getByText('No repository data available.')).toBeDefined(); + }); + + it('correctly ranks repositories and limits to top 5 based on weighted impact score', () => { + // Score formulas: (commits * 3) + (stars * 5) + (forks * 10) + const repos = [ + { name: 'Repo1', commits: 10, stars: 10, forks: 10, createdAt: getRelativeDateString(10) }, // 30 + 50 + 100 = 180 + { name: 'Repo2', commits: 50, stars: 20, forks: 5, createdAt: getRelativeDateString(10) }, // 150 + 100 + 50 = 300 + { name: 'Repo3', commits: 5, stars: 2, forks: 0, createdAt: getRelativeDateString(10) }, // 15 + 10 + 0 = 25 + { name: 'Repo4', commits: 100, stars: 100, forks: 50, createdAt: getRelativeDateString(10) },// 300 + 500 + 500 = 1300 + { name: 'Repo5', commits: 20, stars: 50, forks: 12, createdAt: getRelativeDateString(10) }, // 60 + 250 + 120 = 430 + { name: 'Repo6', commits: 80, stars: 40, forks: 8, createdAt: getRelativeDateString(10) }, // 240 + 200 + 80 = 520 + ]; + + render(); + + // Top 5 rank order expected: Repo4 (1300), Repo6 (520), Repo5 (430), Repo2 (300), Repo1 (180) + // Repo3 (25) should be excluded (ranked 6th) + + expect(screen.getByText('Repo4')).toBeDefined(); + expect(screen.getByText('Repo6')).toBeDefined(); + expect(screen.getByText('Repo5')).toBeDefined(); + expect(screen.getByText('Repo2')).toBeDefined(); + expect(screen.getByText('Repo1')).toBeDefined(); + expect(screen.queryByText('Repo3')).toBeNull(); + + // Verify correct score calculations are rendered + expect(screen.getByText('1300')).toBeDefined(); + expect(screen.getByText('520')).toBeDefined(); + expect(screen.getByText('430')).toBeDefined(); + expect(screen.getByText('300')).toBeDefined(); + expect(screen.getByText('180')).toBeDefined(); + }); + + it('accurately calculates and renders growth metrics', () => { + // Create a repo exactly 10 months old + const tenMonthsAgo = getRelativeDateString(10); + const repos = [ + { + name: 'Repo-Growth', + commits: 50, + stars: 30, + forks: 20, + createdAt: tenMonthsAgo, + }, + ]; + + render(); + + // Individual repo metrics verification: + // Age = 10 months + expect(screen.getAllByText('10 months').length).toBeGreaterThanOrEqual(1); + + // Overall growth metrics cards verification: + // Avg Stars/Mo overall = 30 / 10 = 3 + // Avg Forks/Mo overall = 20 / 10 = 2 + expect(screen.getAllByText('3').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('2').length).toBeGreaterThanOrEqual(1); + }); + + it('computes language contributions percentage correctly', () => { + const repos = [ + { + name: 'TS-Repo', + commits: 70, + primaryLanguage: { name: 'TypeScript', color: '#3178c6' }, + }, + { + name: 'JS-Repo', + commits: 30, + primaryLanguage: { name: 'JavaScript', color: '#f1e05a' }, + }, + ]; + + render(); + + // Total commits = 100. TypeScript = 70%, JavaScript = 30% + expect(screen.getAllByText('TypeScript').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('JavaScript').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('70%').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('30%').length).toBeGreaterThanOrEqual(1); + }); + + it('complies with accessibility (a11y) standards', () => { + const repos = [ + { + name: 'A11y-Repo', + commits: 10, + stars: 5, + forks: 2, + createdAt: getRelativeDateString(5), + }, + ]; + + const { container } = render(); + + // Region role and labelling check + const region = screen.getByRole('region'); + expect(region).toBeDefined(); + expect(region.getAttribute('aria-labelledby')).toBe('impact-analyzer-title'); + + // Heading has matching id + const heading = screen.getByRole('heading', { level: 3 }); + expect(heading.id).toBe('impact-analyzer-title'); + + // Language bar has image role with description + const imgElement = screen.getByRole('img'); + expect(imgElement).toBeDefined(); + expect(imgElement.getAttribute('aria-label')).toContain('Language contribution'); + + // Ranking link has informative aria-label + const link = screen.getByRole('link'); + expect(link.getAttribute('aria-label')).toContain('Rank 1'); + }); + + it('formats age correctly via helper function', () => { + const mockT = (key: string) => { + if (key === 'dashboard.impact.months') return 'months'; + if (key === 'dashboard.impact.years') return 'years'; + return key; + }; + + expect(formatAge(5, mockT)).toBe('5 months'); + expect(formatAge(12, mockT)).toBe('1 year'); + expect(formatAge(24, mockT)).toBe('2 years'); + expect(formatAge(15, mockT)).toBe('1y 3m'); + }); +}); diff --git a/components/dashboard/RepositoryImpactAnalyzer.tsx b/components/dashboard/RepositoryImpactAnalyzer.tsx new file mode 100644 index 000000000..f9d58a083 --- /dev/null +++ b/components/dashboard/RepositoryImpactAnalyzer.tsx @@ -0,0 +1,347 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { Award, GitFork, Star, TrendingUp, Calendar, BookOpen } from 'lucide-react'; +import { useTranslation } from '@/context/TranslationContext'; + +export interface RepositoryImpactAnalyzerProps { + repositories: Array<{ + name: string; + commits?: number; + commitCount?: number; + stars?: number; + stargazerCount?: number; + forks?: number; + forkCount?: number; + createdAt?: string | Date; + created_at?: string | Date; + language?: { + name: string; + color: string; + } | string | null; + primaryLanguage?: { + name: string; + color: string; + } | string | null; + url?: string; + }>; +} + +export function formatAge(months: number, t: (key: string) => string): string { + if (months < 12) { + return `${months} ${t('dashboard.impact.months')}`; + } + const years = Math.floor(months / 12); + const remainingMonths = months % 12; + if (remainingMonths === 0) { + const unit = years === 1 ? t('dashboard.impact.years').replace(/s$/, '') : t('dashboard.impact.years'); + return `${years} ${unit}`; + } + return `${years}y ${remainingMonths}m`; +} + +export default function RepositoryImpactAnalyzer({ repositories = [] }: RepositoryImpactAnalyzerProps) { + const { t } = useTranslation(); + + // Handle empty state gracefully + if (!repositories || repositories.length === 0) { + return ( + +

+ + {t('dashboard.impact.title')} +

+

+ {t('dashboard.impact.no_data')} +

+
+ ); + } + + // Sanitize and process repositories + const processedRepos = repositories.map((repo) => { + const commits = repo.commits ?? repo.commitCount ?? 0; + const stars = repo.stars ?? repo.stargazerCount ?? 0; + const forks = repo.forks ?? repo.forkCount ?? 0; + + // Resolve Language name and color + let langName = 'Unknown'; + let langColor = '#94a3b8'; // default slate-400 + + const langObj = repo.primaryLanguage ?? repo.language; + if (langObj) { + if (typeof langObj === 'object') { + langName = langObj.name ?? 'Unknown'; + langColor = langObj.color ?? '#94a3b8'; + } else if (typeof langObj === 'string') { + langName = langObj; + } + } + + // Resolve repository age in months + const now = new Date(); + const createdRaw = repo.createdAt ?? repo.created_at ?? now; + const created = new Date(createdRaw); + const validCreated = isNaN(created.getTime()) ? now : created; + const diffYears = now.getFullYear() - validCreated.getFullYear(); + const diffMonths = now.getMonth() - validCreated.getMonth(); + // age must be at least 1 month to avoid division by zero + const ageInMonths = Math.max(1, diffYears * 12 + diffMonths); + + // Calculate impact score: (commits * 3) + (stars * 5) + (forks * 10) + const score = commits * 3 + stars * 5 + forks * 10; + + const avgStarsPerMonth = parseFloat((stars / ageInMonths).toFixed(2)); + const avgForksPerMonth = parseFloat((forks / ageInMonths).toFixed(2)); + + return { + name: repo.name, + commits, + stars, + forks, + score, + ageInMonths, + avgStarsPerMonth, + avgForksPerMonth, + language: { + name: langName, + color: langColor, + }, + url: repo.url ?? '#', + }; + }); + + // Rank Top 5 Repositories based on Score + const topRepos = [...processedRepos].sort((a, b) => b.score - a.score).slice(0, 5); + + // Group commits by repository language + const languageGroups: Record = {}; + let totalCommits = 0; + + processedRepos.forEach((repo) => { + const lang = repo.language.name; + const color = repo.language.color; + const commits = repo.commits; + + totalCommits += commits; + if (!languageGroups[lang]) { + languageGroups[lang] = { commits: 0, color }; + } + languageGroups[lang].commits += commits; + }); + + const languageContribution = Object.entries(languageGroups) + .map(([name, data]) => { + const percentage = totalCommits > 0 ? parseFloat(((data.commits / totalCommits) * 100).toFixed(1)) : 0; + return { + name, + commits: data.commits, + color: data.color, + percentage, + }; + }) + .sort((a, b) => b.commits - a.commits); + + // Calculate Overall Growth Metrics + const totalAgeMonths = processedRepos.reduce((acc, repo) => acc + repo.ageInMonths, 0); + const avgAgeMonths = processedRepos.length > 0 ? totalAgeMonths / processedRepos.length : 0; + const totalStars = processedRepos.reduce((acc, repo) => acc + repo.stars, 0); + const totalForks = processedRepos.reduce((acc, repo) => acc + repo.forks, 0); + + const avgStarsPerMonthOverall = totalAgeMonths > 0 ? parseFloat((totalStars / totalAgeMonths).toFixed(2)) : 0; + const avgForksPerMonthOverall = totalAgeMonths > 0 ? parseFloat((totalForks / totalAgeMonths).toFixed(2)) : 0; + + return ( + + {/* Title Header */} +
+
+
+ +
+
+

+ {t('dashboard.impact.title')} +

+

+ {t('dashboard.impact.growth')} & ranking analysis +

+
+
+
+ + {/* Main Grid Layout */} +
+ + {/* Left Column: Top 5 Repositories ranking */} + + + {/* Right Column: Language Contribution & Overall Growth Metrics */} +
+ + {/* Language Contribution */} +
+

+ {t('dashboard.impact.languages')} +

+ + {/* Responsive Segment Bar */} +
+ {languageContribution.map((lang) => ( + + ))} +
+ + {/* Language Legend */} +
+ {languageContribution.slice(0, 4).map((lang) => ( +
+
+ + {lang.name} +
+ + {lang.percentage}% + +
+ ))} +
+
+ + {/* Growth Metrics Summary Cards */} +
+

+ + {t('dashboard.impact.growth')} +

+ +
+ {/* Avg Age */} +
+
+ + Age +
+
+ {formatAge(Math.round(avgAgeMonths), t)} +
+
+ + {/* Avg Stars / Month */} +
+
+ + Stars/Mo +
+
+ {avgStarsPerMonthOverall} +
+
+ + {/* Avg Forks / Month */} +
+
+ + Forks/Mo +
+
+ {avgForksPerMonthOverall} +
+
+
+
+ +
+ +
+
+ ); +} diff --git a/locales/de.json b/locales/de.json index b25f110ae..1a395b371 100644 --- a/locales/de.json +++ b/locales/de.json @@ -249,6 +249,129 @@ "github_wrapped": "GitHub Wrapped", "download_webp": "Optimiertes WebP herunterladen", "download_stl": "Druckbares 3D-STL herunterladen" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Beitragsprognose", + "subtitle": "Prognostizieren Sie zukünftiges Wachstum basierend auf Ihrer Programmiergeschwindigkeit und Ihren Trendsteigungen.", + "weekly_velocity": "Wöchentliche Geschwindigkeit", + "monthly_velocity": "Monatliche Geschwindigkeit", + "projected_month": "Prognostiziertes Monatsende", + "projected_year": "Prognostiziertes Jahresende", + "consistency_rating": "Konsistenzbewertung", + "trend_direction": "Trend", + "commits_per_week": "{{count}} Commits/Woche", + "commits_per_month": "{{count}} Commits/Monat", + "commits": "{{count}} Commits", + "growth_rate": "Wachstumsrate: {{rate}} Commits/Tag²", + "consistency_level": { + "elite": "Elite (Sehr konsistent)", + "consistent": "Hoch (Konsistent)", + "occasional": "Moderat (Gelegentlich)", + "sporadic": "Niedrig (Sporadisch)", + "inactive": "Inaktiv" + }, + "trends": { + "strong_growth": "Starkes Wachstum", + "moderate_growth": "Moderates Wachstum", + "stable": "Stabiler Rhythmus", + "cooling": "Abkühlung", + "decline": "Deutlicher Rückgang" + }, + "no_activity": "Keine vergangenen Aktivitätsdaten verfügbar, um Vorhersagen zu generieren." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/en.json b/locales/en.json index 5d6fd780d..6809128d5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -249,6 +249,129 @@ "github_wrapped": "GitHub Wrapped", "download_webp": "Download Optimized WebP", "download_stl": "Download Printable 3D STL" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Contribution Forecast", + "subtitle": "Predict future growth based on your coding velocity and trend slopes.", + "weekly_velocity": "Weekly Velocity", + "monthly_velocity": "Monthly Velocity", + "projected_month": "Projected Month-End", + "projected_year": "Projected Year-End", + "consistency_rating": "Consistency Rating", + "trend_direction": "Trend", + "commits_per_week": "{{count}} Commits/Week", + "commits_per_month": "{{count}} Commits/Month", + "commits": "{{count}} Commits", + "growth_rate": "Growth Rate: {{rate}} Commits/Day²", + "consistency_level": { + "elite": "Elite (Very Consistent)", + "consistent": "High (Consistent)", + "occasional": "Moderate (Occasional)", + "sporadic": "Low (Sporadic)", + "inactive": "Inactive" + }, + "trends": { + "strong_growth": "Strong Growth", + "moderate_growth": "Moderate Growth", + "stable": "Stable Rhythm", + "cooling": "Cooling", + "decline": "Significant Decline" + }, + "no_activity": "No past activity data available to generate predictions." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/es.json b/locales/es.json index fd5ec9ca6..58d665a8c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -8,7 +8,8 @@ "menu_close": "Cerrar menú", "compare": "Comparar", "customization_studio": "Estudio de Personalización", - "generator": "Generador" + "generator": "Generador", + "burnout_radar": "Burnout Radar" }, "footer": { "tagline": "Diseñado para la comunidad de desarrolladores de élite.", @@ -65,11 +66,13 @@ "preview_monolith": "MONOLITO DE PREVISUALIZACIÓN", "interactive_preview_title": "Previsualización interactiva del monolito", "interactive_preview_desc": "CommitPulse compila tu historial de contribuciones públicas de GitHub en una ciudad 3D customizable. Cuanto más altas son las torres, más commits hiciste ese dá. Ingresa un nombre de usuario de GitHub arriba para generar instantáneamente tu insignia.", - "input_aria_label": "Ingresa el usuario de GitHub para generar la insignia" + "input_aria_label": "Ingresa el usuario de GitHub para generar la insignia", + "unable_to_load_stats": "Unable to load stats" }, "success_guide": { "title": "Tu monolito está listo - Despliégalo en 4 pasos", "markdown_copied": "Markdown copiado", + "dismiss_aria": "Dismiss guide", "step_1_title": "Abre el repositorio de tu perfil", "step_1_body": "Navega a github.com/TU_USUARIO/TU_USUARIO - tu repositorio de perfil especial.", "step_2_title": "Edita el README.md", @@ -132,7 +135,9 @@ "copy_aria_disabled": "Agrega un usuario de GitHub para habilitar la copia del fragmento de exportación {{format}}", "copy_aria_enabled": "Copiar fragmento de exportación {{format}} al portapapeles", "footer_tip": "Pega esto en el README.md de tu perfil de GitHub. La insignia se genera en el servidor, no requiere scripts.", - "tsx": "React TSX" + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" } }, "dashboard": { @@ -166,7 +171,9 @@ "loc": "Líneas de código", "loc_desc": "Líneas de código modificadas a lo largo del tiempo", "commits_desc": "Frecuencia de commits a lo largo del tiempo", - "lines_modified": "{{count}} líneas modificadas" + "lines_modified": "{{count}} líneas modificadas", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" }, "languages": { "title": "Idiomas principales", @@ -237,7 +244,134 @@ "svg_downloaded": "¡SVG descargado!", "json_downloaded": "¡JSON descargado!", "failed": "Error — intenta de nuevo", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", "download_stl": "Descargar monolito 3D imprimible STL" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Pronóstico de Contribuciones", + "subtitle": "Prediga el crecimiento futuro basado en su velocidad de programación y pendientes de tendencia.", + "weekly_velocity": "Velocidad Semanal", + "monthly_velocity": "Velocidad Mensual", + "projected_month": "Fin de Mes Proyectado", + "projected_year": "Fin de Año Proyectado", + "consistency_rating": "Calificación de Consistencia", + "trend_direction": "Tendencia", + "commits_per_week": "{{count}} commits/sem", + "commits_per_month": "{{count}} commits/mes", + "commits": "{{count}} commits", + "growth_rate": "Tasa de Crecimiento: {{rate}} commits/día²", + "consistency_level": { + "elite": "Élite (Muy Consistente)", + "consistent": "Alta (Consistente)", + "occasional": "Moderada (Ocasional)", + "sporadic": "Baja (Esporádica)", + "inactive": "Inactivo" + }, + "trends": { + "strong_growth": "Crecimiento Fuerte", + "moderate_growth": "Crecimiento Moderado", + "stable": "Ritmo Estable", + "cooling": "Enfriamiento", + "decline": "Disminución Significativa" + }, + "no_activity": "No hay datos de actividad pasada disponibles para generar predicciones." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/fr.json b/locales/fr.json index 27bd4d7bb..1f0c860ab 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -8,7 +8,8 @@ "menu_close": "Fermer le menu", "compare": "Comparer", "customization_studio": "Studio de Personnalisation", - "generator": "Générateur" + "generator": "Générateur", + "burnout_radar": "Burnout Radar" }, "footer": { "tagline": "Conçu pour la communauté des développeurs d'élite.", @@ -32,6 +33,8 @@ "generator": "Générateur" }, "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", "title": "Sublimez Votre\nHistoire de {Contributions}.", "subtitle": "Ne vous contentez plus de grilles plates. Générez des monolithes 3D isométriques haute fidélité qui visualisent votre rythme de codage avec une précision professionnelle.", "watch_dashboard": "Voir le tableau de bord", @@ -53,7 +56,9 @@ "sync_title": "Synchronisation en temps réel", "sync_desc": "Extrait directement de l'API GraphQL de GitHub. Votre série de commits se met à jour aussi vite que vous poussez votre code.", "theme_title": "Moteur de Thèmes", - "theme_desc": "Basculez entre les modes Néon, Dracula ou HEX personnalisés via une gestion simple des URL." + "theme_desc": "Basculez entre les modes Néon, Dracula ou HEX personnalisés via une gestion simple des URL.", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." }, "generate_badge": "Générer le badge", "verified_profile": "Profil vérifié", @@ -61,11 +66,13 @@ "preview_monolith": "APERCU DU MONOLITHE", "interactive_preview_title": "Aperçu interactif du monolithe", "interactive_preview_desc": "CommitPulse compile votre historique de contributions GitHub publiques dans une ville 3D personnalisable. Plus les tours sont hautes, plus vous avez contribué ce jour-là. Entrez un nom d'utilisateur GitHub ci-dessus pour générer instantanément votre badge de streak.", - "input_aria_label": "Entrez le nom d'utilisateur GitHub pour générer le badge" + "input_aria_label": "Entrez le nom d'utilisateur GitHub pour générer le badge", + "unable_to_load_stats": "Unable to load stats" }, "success_guide": { "title": "Votre Monolithe est Prêt - Déployez-le en 4 étapes", "markdown_copied": "Markdown copié", + "dismiss_aria": "Dismiss guide", "step_1_title": "Ouvrez le dépôt de votre profil", "step_1_body": "Rendez-vous sur github.com/VOTRE_PSEUDO/VOTRE_PSEUDO - votre dépôt de profil spécial.", "step_2_title": "Modifiez le fichier README.md", @@ -108,19 +115,29 @@ "sync_year": "Année de synchronisation", "clear_custom": "Effacer les couleurs personnalisées", "theme_presets": "Préréglage de thème", - "color_overrides": "Surcharges de couleurs personnalisées" + "color_overrides": "Surcharges de couleurs personnalisées", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" }, "export": { "snippet_title": "Extrait d'exportation", "snippet_desc": "Basculez entre les formats sans modifier la configuration du badge en direct.", "markdown": "Markdown", "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", "copy_format": "Copier le {{format}}", "copied": "Copié !", "copy_aria_disabled": "Ajoutez un nom d'utilisateur GitHub pour activer la copie de l'extrait d'exportation {{format}}", "copy_aria_enabled": "Copier l'extrait d'exportation {{format}} dans le presse-papiers", "footer_tip": "Collez ceci dans le fichier README.md de votre profil GitHub. Le badge est généré côté serveur, aucun script n'est requis.", - "tsx": "React TSX" + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" } }, "dashboard": { @@ -135,7 +152,9 @@ "following": "Abonnements", "stars": "Étoiles", "joined": "Rejoint en", - "pro_badge": "PRO" + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" }, "achievements": { "title": "Succès", @@ -152,7 +171,9 @@ "loc": "Lignes de code", "loc_desc": "Lignes de code modifiées au fil du temps", "commits_desc": "Fréquence des commits au fil du temps", - "lines_modified": "{{count}} lignes modifiées" + "lines_modified": "{{count}} lignes modifiées", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" }, "languages": { "title": "Principales langues", @@ -223,7 +244,134 @@ "svg_downloaded": "SVG téléchargé !", "json_downloaded": "JSON téléchargé !", "failed": "Échec — réessayez", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", "download_stl": "Télécharger le monolithe STL 3D imprimable" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Prévisions de Contribution", + "subtitle": "Prédisez la croissance future en fonction de votre vitesse de codage et des pentes de tendance.", + "weekly_velocity": "Vitesse Hebdomadaire", + "monthly_velocity": "Vitesse Mensuelle", + "projected_month": "Fin de Mois Projetée", + "projected_year": "Fin d'Année Projetée", + "consistency_rating": "Indice de Cohérence", + "trend_direction": "Tendance", + "commits_per_week": "{{count}} commits/semaine", + "commits_per_month": "{{count}} commits/mois", + "commits": "{{count}} commits", + "growth_rate": "Taux de Croissance: {{rate}} commits/jour²", + "consistency_level": { + "elite": "Élite (Très Cohérent)", + "consistent": "Élevé (Cohérent)", + "occasional": "Modéré (Occasionnel)", + "sporadic": "Faible (Sporadique)", + "inactive": "Inactif" + }, + "trends": { + "strong_growth": "Forte Croissance", + "moderate_growth": "Croissance Modérée", + "stable": "Rythme Stable", + "cooling": "Ralentissement", + "decline": "Baisse Significative" + }, + "no_activity": "Aucune donnée d'activité passée disponible pour générer des prévisions." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/hi.json b/locales/hi.json index 06374cbb9..4e14a68c2 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -8,7 +8,8 @@ "menu_close": "मेन्यू बंद करें", "compare": "तुलना करें", "customization_studio": "कस्टमाइजेशन स्टूडियो", - "generator": "जनरेटर" + "generator": "जनरेटर", + "burnout_radar": "Burnout Radar" }, "footer": { "tagline": "उत्कृष्ट डेवलपर समुदाय के लिए डिज़ाइन किया गया।", @@ -55,7 +56,9 @@ "sync_title": "रीयल-टाइम सिंक", "sync_desc": "सीधे गिटहब ग्राफक्यूएल एपीआई से प्राप्त। जैसे ही आप कोड पुश करते हैं, आपकी स्ट्रीक अपडेट हो जाती है।", "theme_title": "थीम इंजन", - "theme_desc": "सरल यूआरएल प्रबंधन के माध्यम से नियॉन, ड्रैकुला, या कस्टम हेक्स मोड के बीच स्विच करें।" + "theme_desc": "सरल यूआरएल प्रबंधन के माध्यम से नियॉन, ड्रैकुला, या कस्टम हेक्स मोड के बीच स्विच करें।", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." }, "generate_badge": "बैज उत्पन्न करें", "verified_profile": "सत्यापित प्रोफ़ाइल", @@ -63,11 +66,13 @@ "preview_monolith": "प्रोटोकॉल मोनोलिथ", "interactive_preview_title": "इंटरएक्टिव मोनोलिथ पूर्वावलोकन", "interactive_preview_desc": "CommitPulse आपके सार्वजनिक GitHub योगदान इतिहास को एक अनुकूलन योग्य 3D शहर में संकलित करता है। टावर जितने ऊंचे होंगे, आपने उस दिन उतने ही अधिक कमिट किए॥ अपनी स्ट्रीक बैज को त्रुत उत्पन्न करने के लिए ऊपर एक GitHub उपभोक्ता नाम दर्ज करें॥", - "input_aria_label": "बैज जनरेट करने के लिए गिटहब यूजरनेम दर्ज करें" + "input_aria_label": "बैज जनरेट करने के लिए गिटहब यूजरनेम दर्ज करें", + "unable_to_load_stats": "Unable to load stats" }, "success_guide": { "title": "आपका मोनोलिथ तैयार है - इसे 4 चरणों में परिनियोजित करें", "markdown_copied": "मार्कडाउन कॉपी किया गया", + "dismiss_aria": "Dismiss guide", "step_1_title": "अपनी प्रोफ़ाइल रेपो खोलें", "step_1_body": "github.com/YOUR_USERNAME/YOUR_USERNAME पर जाएं - आपकी विशेष प्रोफ़ाइल रिपॉजिटरी।", "step_2_title": "README.md संपादित करें", @@ -130,7 +135,9 @@ "copy_aria_disabled": "निर्यात स्निपेट {{format}} कॉपी करने के लिए गिटहब यूजरनेम जोड़ें", "copy_aria_enabled": "निर्यात स्निपेट {{format}} क्लिपबोर्ड पर कॉपी करें", "footer_tip": "इसे अपनी गिटहब प्रोफ़ाइल के README.md में पेस्ट करें। बैज सर्वर-साइड रेंडर होता है, किसी स्क्रिप्ट की आवश्यकता नहीं है।", - "tsx": "React TSX" + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" } }, "dashboard": { @@ -145,7 +152,9 @@ "following": "फ़ॉलोइंग", "stars": "तारे (Stars)", "joined": "शामिल हुए", - "pro_badge": "प्रो" + "pro_badge": "प्रो", + "pro": "PRO", + "share": "Share Your Pulse" }, "achievements": { "title": "उपलब्धियां", @@ -162,7 +171,9 @@ "loc": "कोड की पंक्तियां", "loc_desc": "समय के साथ संशोधित कोड की पंक्तियां", "commits_desc": "समय के साथ कमिट की आवृत्ति", - "lines_modified": "{{count}} पंक्तियां संशोधित" + "lines_modified": "{{count}} पंक्तियां संशोधित", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" }, "languages": { "title": "शीर्ष भाषाएँ", @@ -233,7 +244,134 @@ "svg_downloaded": "SVG डाउनलोड किया गया!", "json_downloaded": "JSON डाउनलोड किया गया!", "failed": "विफल — पुनः प्रयास करें", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", "download_stl": "प्रिंट करने योग्य 3D STL मोनोलिथ डाउनलोड करें" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "योगदान का पूर्वानुमान", + "subtitle": "अपने कोडिंग वेग और प्रवृत्ति ढलानों के आधार पर भविष्य के विकास का पूर्वानुमान लगाएं।", + "weekly_velocity": "साप्ताहिक वेग", + "monthly_velocity": "मासिक वेग", + "projected_month": "अनुमानित महीना-अंत", + "projected_year": "अनुमानित वर्ष-अंत", + "consistency_rating": "स्थिरता रेटिंग", + "trend_direction": "प्रवृत्ति", + "commits_per_week": "{{count}} कमिट/सप्ताह", + "commits_per_month": "{{count}} कमिट/महीना", + "commits": "{{count}} कमिट", + "growth_rate": "विकास दर: {{rate}} कमिट/दिन²", + "consistency_level": { + "elite": "उत्कृष्ट (अति सुसंगत)", + "consistent": "उच्च (सुसंगत)", + "occasional": "मध्यम (प्रासंगिक)", + "sporadic": "कम (छिटपुट)", + "inactive": "निष्क्रिय" + }, + "trends": { + "strong_growth": "मजबूत विकास", + "moderate_growth": "मध्यम विकास", + "stable": "स्थिर लय", + "cooling": "धीमा हो रहा है", + "decline": "महत्वपूर्ण गिरावट" + }, + "no_activity": "भविष्यवाणी उत्पन्न करने के लिए कोई पिछला गतिविधि डेटा उपलब्ध नहीं है।" + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/ja.json b/locales/ja.json index b32520d34..a3f981304 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -8,7 +8,8 @@ "menu_close": "メニューを閉じる", "compare": "比較する", "customization_studio": "カスタマイズスタジオ", - "generator": "ジェネレーター" + "generator": "ジェネレーター", + "burnout_radar": "Burnout Radar" }, "footer": { "tagline": "エリートビルダーコミュニティ向けに設計されています。", @@ -32,6 +33,8 @@ "generator": "ジェネレーター" }, "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", "title": "あなたの{開発の軌跡}を\n立体的に表現する。", "subtitle": "平面的なグリッドだけで満足していませんか? あなたのコーディングのリズムをプロフェッショナルな精度で可視化する、高精度な3Dアイソメトリックモノリスを作成しましょう。", "watch_dashboard": "ダッシュボードを見る", @@ -53,7 +56,9 @@ "sync_title": "リアルタイム同期", "sync_desc": "GitHub GraphQL APIから直接取得。あなたの開発ストリークは、コードのプッシュに合わせて即座に更新されます。", "theme_title": "テーマエンジン", - "theme_desc": "シンプルなURLパラメータ管理で、Neon、Dracula、またはカスタムHEXモードを切り替えることができます。" + "theme_desc": "シンプルなURLパラメータ管理で、Neon、Dracula、またはカスタムHEXモードを切り替えることができます。", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." }, "generate_badge": "バッジを生成", "verified_profile": "確認済みプロファイル", @@ -61,11 +66,13 @@ "preview_monolith": "モノリスプレビュー", "interactive_preview_title": "インタラクティブなモノリスプレビュー", "interactive_preview_desc": "CommitPulseは、公開されているGitHubのコントリビューション履歴を、カスタマイズ可能な3D都市にコンパイルします。タワーが高いほど、その日のコミット数が多いことを示します。上にGitHubのユーザー名を入力して、今すぐストリークバッジを生成してください。", - "input_aria_label": "バッジを生成するにはGitHubユーザー名を入力してください" + "input_aria_label": "バッジを生成するにはGitHubユーザー名を入力してください", + "unable_to_load_stats": "Unable to load stats" }, "success_guide": { "title": "モノリスの準備ができました - 4つのステップで配置しましょう", "markdown_copied": "Markdownがコピーされました", + "dismiss_aria": "Dismiss guide", "step_1_title": "プロフィールリポジトリを開く", "step_1_body": "github.com/あなたのユーザー名/あなたのユーザー名 - あなたの特別なプロフィールリポジトリに移動します。", "step_2_title": "README.mdを編集する", @@ -108,19 +115,29 @@ "sync_year": "同期対象年", "clear_custom": "カスタムカラーをクリア", "theme_presets": "テーマプリセット", - "color_overrides": "カスタムカラー上書き" + "color_overrides": "カスタムカラー上書き", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" }, "export": { "snippet_title": "エクスポートスニペット", "snippet_desc": "バッジのライブ設定を変更することなく、エクスポート形式を切り替えることができます。", "markdown": "Markdown", "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", "copy_format": "{{format}}をコピー", "copied": "コピーされました!", "copy_aria_disabled": "GitHubユーザー名を追加して{{format}}エクスポートスニペットをコピー", "copy_aria_enabled": "{{format}}エクスポートスニペットをクリップボードにコピー", "footer_tip": "これをGitHubプロフィールのREADME.mdに貼り付けます。バッジはサーバーサイドでレンダリングされるため、スクリプトは不要です。", - "tsx": "React TSX" + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" } }, "dashboard": { @@ -135,7 +152,9 @@ "following": "フォロー数", "stars": "スター獲得数", "joined": "登録日", - "pro_badge": "PRO" + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" }, "achievements": { "title": "実績", @@ -152,7 +171,9 @@ "loc": "コード行数", "loc_desc": "期間中に変更されたコード行数", "commits_desc": "期間中のコミット颻度", - "lines_modified": "{{count}} 行変更" + "lines_modified": "{{count}} 行変更", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" }, "languages": { "title": "主な言語", @@ -223,7 +244,134 @@ "svg_downloaded": "SVGがダウンロードされました!", "json_downloaded": "JSONがダウンロードされました!", "failed": "失敗しました — 再試行してください", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", "download_stl": "印刷可能な3D STLモノリスのダウンロード" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "貢献予測", + "subtitle": "コーディング速度とトレンドの傾きに基づいて将来의成長を予測します。", + "weekly_velocity": "週平均ベロシティ", + "monthly_velocity": "月平均ベロシティ", + "projected_month": "月末の予測値", + "projected_year": "年末の予測値", + "consistency_rating": "継続性評価", + "trend_direction": "トレンド", + "commits_per_week": "{{count}} コミット/週", + "commits_per_month": "{{count}} コミット/月", + "commits": "{{count}} コミット", + "growth_rate": "成長率: {{rate}} コミット/日²", + "consistency_level": { + "elite": "エリート (非常に安定)", + "consistent": "高い (安定)", + "occasional": "適度 (時々)", + "sporadic": "低い (散発的)", + "inactive": "非アクティブ" + }, + "trends": { + "strong_growth": "強い成長", + "moderate_growth": "緩やかな成長", + "stable": "安定したリズム", + "cooling": "減速中", + "decline": "大幅な低下" + }, + "no_activity": "予測を生成するための過去のアクティビティデータがありません。" + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/ko.json b/locales/ko.json index 8d4a5b5c0..546ea7a24 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -8,7 +8,8 @@ "menu_close": "메뉴 닫기", "compare": "비교하기", "customization_studio": "커스터마이징 스튜디오", - "generator": "제너레이터" + "generator": "제너레이터", + "burnout_radar": "Burnout Radar" }, "footer": { "tagline": "엘리트 빌더 커뮤니티를 위해 설계되었습니다.", @@ -32,6 +33,8 @@ "generator": "제너레이터" }, "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", "title": "{기여} 내역을\n더 돋보이게.", "subtitle": "평면 그리드에 만족하지 마세요. 코딩 리듬을 전문적인 정밀도로 시각화해주는 고해상도 3D 등각 모놀리스를 생성하세요.", "watch_dashboard": "대시보드 보기", @@ -53,7 +56,9 @@ "sync_title": "실시간 동기화", "sync_desc": "GitHub GraphQL API에서 직접 가져옵니다. 코드를 푸시하는 것과 동시에 스트리크가 업데이트됩니다.", "theme_title": "테마 엔진", - "theme_desc": "간단한 URL 매개변수 관리를 통해 Neon, Dracula 또는 사용자 정의 HEX 모드 간에 전환하세요." + "theme_desc": "간단한 URL 매개변수 관리를 통해 Neon, Dracula 또는 사용자 정의 HEX 모드 간에 전환하세요.", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." }, "generate_badge": "빓지 생성", "verified_profile": "인증된 프로필", @@ -61,11 +66,13 @@ "preview_monolith": "모놀리스 미리보기", "interactive_preview_title": "대화형 모놀리스 미리보기", "interactive_preview_desc": "CommitPulse는 공개 GitHub 기여 내역음 맞춤설정 가능한 3D 도시로 컴파일합니다. 타워가 높을수록 그날 더 많은 커밋을 한 것입니다. 위에 GitHub 사용자 이름을 입력하여 지금 바로 스트리크 밷지리 생성하세요.", - "input_aria_label": "배지를 생성하려면 GitHub 사용자 이름을 입력하세요" + "input_aria_label": "배지를 생성하려면 GitHub 사용자 이름을 입력하세요", + "unable_to_load_stats": "Unable to load stats" }, "success_guide": { "title": "모놀리스가 준비되었습니다 - 4단계로 배포하세요", "markdown_copied": "Markdown이 복사되었습니다", + "dismiss_aria": "Dismiss guide", "step_1_title": "프로필 저장소 열기", "step_1_body": "github.com/사용자이름/사용자이름 - 특수 프로필 저장소로 이동합니다.", "step_2_title": "README.md 수정", @@ -108,19 +115,29 @@ "sync_year": "동기화 연도", "clear_custom": "사용자 정의 색상 지우기", "theme_presets": "테마 프리셋", - "color_overrides": "사용자 정의 색상 덮어쓰기" + "color_overrides": "사용자 정의 색상 덮어쓰기", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" }, "export": { "snippet_title": "내보내기 스니펫", "snippet_desc": "라이브 배지 설정을 변경하지 않고 내보내기 형식을 전환하세요.", "markdown": "Markdown", "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", "copy_format": "{{format}} 복사", "copied": "복사됨!", "copy_aria_disabled": "내보내기 스니펫 {{format}}을 복사하려면 GitHub 사용자 이름을 추가하세요", "copy_aria_enabled": "{{format}} 내보내기 스니펫을 클립보드에 복사", "footer_tip": "이것을 GitHub 프로필 README.md 파일에 붙여넣으세요. 배지는 서버 측에서 렌더링되므로 스크립트가 필요하지 않습니다.", - "tsx": "React TSX" + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" } }, "dashboard": { @@ -135,7 +152,9 @@ "following": "팔로잉", "stars": "스타 수", "joined": "가입일", - "pro_badge": "PRO" + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" }, "achievements": { "title": "달성 업적", @@ -152,7 +171,9 @@ "loc": "코드 라인 수", "loc_desc": "시간 경과에 따른 수정된 코드 라인 수", "commits_desc": "시간 경과에 따른 커밋 빈도", - "lines_modified": "{{count}} 라인 수정됨" + "lines_modified": "{{count}} 라인 수정됨", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" }, "languages": { "title": "주요 언어", @@ -223,7 +244,134 @@ "svg_downloaded": "SVG가 다운로드되었습니다!", "json_downloaded": "JSON이 다운로드되었습니다!", "failed": "실패 — 다시 시도하세요", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", "download_stl": "인쇄 가능한 3D STL 모놀리스 다운로드" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "기여 예측", + "subtitle": "코딩 속도와 트렌드 기울기를 바탕으로 향후 성장을 예측합니다.", + "weekly_velocity": "주간 속도", + "monthly_velocity": "월간 속도", + "projected_month": "예상 월말 합계", + "projected_year": "예상 연말 합계", + "consistency_rating": "일관성 등급", + "trend_direction": "트렌드", + "commits_per_week": "주당 {{count}}회 커밋", + "commits_per_month": "월당 {{count}}회 커밋", + "commits": "{{count}}회 커밋", + "growth_rate": "성장률: {{rate}} 커밋/일²", + "consistency_level": { + "elite": "엘리트 (매우 일관됨)", + "consistent": "높음 (일관됨)", + "occasional": "보통 (가끔)", + "sporadic": "낮음 (산발적)", + "inactive": "비활성" + }, + "trends": { + "strong_growth": "강한 성장", + "moderate_growth": "완만한 성장", + "stable": "안정적인 리듬", + "cooling": "둔화됨", + "decline": "상당한 감소" + }, + "no_activity": "예측을 생성할 수 있는 이전 활동 데이터가 없습니다." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/pt.json b/locales/pt.json index 18fbca012..688493847 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -249,6 +249,129 @@ "github_wrapped": "Retrospectiva GitHub", "download_webp": "Baixar WebP Otimizado", "download_stl": "Baixar STL 3D para Impressão" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Previsão de Contribuição", + "subtitle": "Preveja o crescimento futuro com base na sua velocidade de codificação e inclinações de tendência.", + "weekly_velocity": "Velocidade Semanal", + "monthly_velocity": "Velocidade Mensual", + "projected_month": "Final de Mês Projetado", + "projected_year": "Final de Ano Projetado", + "consistency_rating": "Classificação de Consistência", + "trend_direction": "Tendência", + "commits_per_week": "{{count}} commits/sem", + "commits_per_month": "{{count}} commits/mês", + "commits": "{{count}} commits", + "growth_rate": "Taxa de Crescimento: {{rate}} commits/dia²", + "consistency_level": { + "elite": "Elite (Muito Consistente)", + "consistent": "Alta (Consistente)", + "occasional": "Moderada (Ocasional)", + "sporadic": "Baixa (Esporádica)", + "inactive": "Inativo" + }, + "trends": { + "strong_growth": "Crescimento Forte", + "moderate_growth": "Crescimento Moderado", + "stable": "Ritmo Estável", + "cooling": "Esfriamento", + "decline": "Declínio Significativo" + }, + "no_activity": "Não há dados de atividade passada disponíveis para gerar previsões." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/locales/zh.json b/locales/zh.json index 96f1559e1..3031ca987 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -8,7 +8,8 @@ "menu_close": "关闭菜单", "compare": "对比", "customization_studio": "自定义工作室", - "generator": "生成器" + "generator": "生成器", + "burnout_radar": "Burnout Radar" }, "footer": { "tagline": "专为精英开发者社区设计。", @@ -32,6 +33,8 @@ "generator": "生成器" }, "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", "title": "提升您的\n{贡献}故事。", "subtitle": "别再满足于扁平的网格。生成高保真的 3D 等距柱状图,以专业的精度可视化您的编码节奏。", "watch_dashboard": "查看仪表盘", @@ -53,7 +56,9 @@ "sync_title": "实时同步", "sync_desc": "直接从 GitHub GraphQL API 获取。您的连续提交天数与代码推送速度保持同步。", "theme_title": "主题引擎", - "theme_desc": "通过简单的 URL 管理在 Neon、Dracula 或自定义十六进制 (HEX) 模式之间进行切换。" + "theme_desc": "通过简单的 URL 管理在 Neon、Dracula 或自定义十六进制 (HEX) 模式之间进行切换。", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." }, "generate_badge": "生成徽章", "verified_profile": "已验证的个人资料", @@ -61,11 +66,13 @@ "preview_monolith": "预览单体", "interactive_preview_title": "交互式单体预览", "interactive_preview_desc": "CommitPulse 将您的公开 GitHub 贡甮历史编译成一个可定制的 3D 城市。塔越高,代表您那天的提交越多。在上方输入 GitHub 用户名即可立即生成您皅连续提交徽章。", - "input_aria_label": "输入 GitHub 用户名以生成徽章" + "input_aria_label": "输入 GitHub 用户名以生成徽章", + "unable_to_load_stats": "Unable to load stats" }, "success_guide": { "title": "您的柱状图已准备就绪 - 仅需 4 步即可部署", "markdown_copied": "Markdown 已复制", + "dismiss_aria": "Dismiss guide", "step_1_title": "打开您的个人主页仓库", "step_1_body": "前往 github.com/您的用户名/您的用户名 - 您的专属个人资料仓库。", "step_2_title": "编辑 README.md", @@ -108,19 +115,29 @@ "sync_year": "同步年份", "clear_custom": "清空自定义颜色", "theme_presets": "主题预设", - "color_overrides": "自定义颜色覆盖" + "color_overrides": "自定义颜色覆盖", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" }, "export": { "snippet_title": "导出片段", "snippet_desc": "在不改变实时徽章配置的情况下切换导出格式。", "markdown": "Markdown", "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", "copy_format": "复制 {{format}}", "copied": "已复制!", "copy_aria_disabled": "添加 GitHub 用户名以复制 {{format}} 导出片段", "copy_aria_enabled": "将 {{format}} 导出片段复制到剪贴板", "footer_tip": "将此片段粘贴到您的 GitHub 个人主页 README.md 文件中。徽章由服务器端渲染,无需任何脚本。", - "tsx": "React TSX" + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" } }, "dashboard": { @@ -135,7 +152,9 @@ "following": "关注数量", "stars": "收获星标", "joined": "加入时间", - "pro_badge": "PRO" + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" }, "achievements": { "title": "个人成就", @@ -152,7 +171,9 @@ "loc": "代码行数", "loc_desc": "随时间变化修改的代码行数", "commits_desc": "随时间变化皅提交频率", - "lines_modified": "已修改 {{count}} 行代码" + "lines_modified": "已修改 {{count}} 行代码", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" }, "languages": { "title": "热门语言", @@ -223,7 +244,134 @@ "svg_downloaded": "SVG 已下载!", "json_downloaded": "JSON 已下载!", "failed": "操作失败 — 请重试", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", "download_stl": "下载可打印的 3D STL 整体结构" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "贡献预测", + "subtitle": "基于您的代码提交速度和趋势斜率预测未来的增长趋势。", + "weekly_velocity": "周平均速度", + "monthly_velocity": "月平均速度", + "projected_month": "预测月末总计", + "projected_year": "预测年末总计", + "consistency_rating": "稳定性评级", + "trend_direction": "趋势", + "commits_per_week": "{{count}} 次提交/周", + "commits_per_month": "{{count}} 次提交/月", + "commits": "{{count}} 次提交", + "growth_rate": "增长率: {{rate}} 次提交/天²", + "consistency_level": { + "elite": "极佳 (非常稳定)", + "consistent": "高 (稳定)", + "occasional": "中 (偶尔)", + "sporadic": "低 (零星)", + "inactive": "未活跃" + }, + "trends": { + "strong_growth": "强劲增长", + "moderate_growth": "温和增长", + "stable": "稳定节奏", + "cooling": "有所放缓", + "decline": "显著下降" + }, + "no_activity": "暂无过去的历史活动数据以生成预测。" + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } } } } diff --git a/scratch/ContributionForecast.test.tsx b/scratch/ContributionForecast.test.tsx new file mode 100644 index 000000000..074a86efa --- /dev/null +++ b/scratch/ContributionForecast.test.tsx @@ -0,0 +1,117 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import ContributionForecast from './ContributionForecast'; +import type { ActivityData } from '@/types/dashboard'; + +// Mock framer-motion to prevent animation issues during testing +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className, whileHover, ...props }: any) => ( +
+ {children} +
+ ), + }, +})); + +// Mock lucide-react +vi.mock('lucide-react', () => ({ + TrendingUp: () =>
, + TrendingDown: () =>
, + Minus: () =>
, + Sparkles: () =>
, + Calendar: () =>
, + Zap: () =>
, + LineChart: () =>
, + Activity: () =>
, + Target: () =>
, +})); + +describe('ContributionForecast', () => { + // Helper for generating sequential active days + const generateMockActivity = (counts: number[], startYear = 2026): ActivityData[] => { + return counts.map((count, index) => { + // pad with leading zero + const dayStr = String(index + 1).padStart(2, '0'); + return { + date: `${startYear}-06-${dayStr}`, + count, + intensity: count > 0 ? 3 : 0, + }; + }); + }; + + it('renders forecast headers and elements', () => { + const activity = generateMockActivity([1, 2, 3]); + render(); + + expect(screen.getByRole('heading', { name: /Contribution Forecast/i })).toBeDefined(); + expect(screen.getByText(/Predict future growth/i)).toBeDefined(); + }); + + it('calculates average weekly and monthly velocities correctly', () => { + // 7 days, 2 commits each day + // total commits = 14. avg daily = 2. weekly = 14. monthly = 60. + const activity = generateMockActivity([2, 2, 2, 2, 2, 2, 2]); + render(); + + expect(screen.getByText('14.0 Commits/Week')).toBeDefined(); + expect(screen.getByText('60.0 Commits/Month')).toBeDefined(); + }); + + it('handles empty activity data gracefully', () => { + render(); + + expect(screen.getByText(/No past activity data available to generate predictions/i)).toBeDefined(); + expect(screen.queryByText('commits/wk')).toBeNull(); + }); + + it('handles zero-commit activity history correctly', () => { + const activity = generateMockActivity([0, 0, 0, 0, 0]); + render(); + + expect(screen.getByText('0.0 Commits/Week')).toBeDefined(); + expect(screen.getByText('0.0 Commits/Month')).toBeDefined(); + expect(screen.getByText('Inactive')).toBeDefined(); + expect(screen.getByText('Stable Rhythm')).toBeDefined(); + + // Projections should equal currentTotal contributions (100) because slope and intercept are 0 + const projectedCommits = screen.getAllByText('100 Commits'); + expect(projectedCommits.length).toBeGreaterThanOrEqual(2); + }); + + it('correctly projects month-end and year-end contributions with linear regression', () => { + // Let's create an increasing activity + // counts: 1 to 10. last entry: 2026-06-10. + // N = 10, meanX = 4.5, meanY = 5.5, slope = 1, intercept = 1 + // End of June (2026-06-30). Days remaining: 20 days. + // Projected daily commits for days 11 to 30: + // y_t = t + 1 (since slope=1, intercept=1). + // For t = 10 to 29 (day index, where day 1 is index 0, day 30 is index 29): + // index for future day is N - 1 + d = 9 + d. + // y_t = (9 + d) * 1 + 1 = 10 + d. + // For d = 1 to 20: 11 + 12 + ... + 30 = 410. + // Current total in activity = 55. + // Projected month end total = 55 + 410 = 465. + const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const activity = generateMockActivity(counts); + + render(); + + // Since totalContributions is not specified, it falls back to sum of activity counts (55). + // Month-End target should show 465 commits. + expect(screen.getByText('465 Commits')).toBeDefined(); + }); + + it('determines consistency rating and trend categories correctly', () => { + // 10 days, 9 active days -> active ratio = 90% -> Elite consistency + const activity = generateMockActivity([1, 2, 3, 0, 4, 5, 6, 7, 8, 9]); + render(); + + expect(screen.getByText('Elite (Very Consistent)')).toBeDefined(); + expect(screen.getByText('90% active days')).toBeDefined(); + + // slope is positive and strong + expect(screen.getByText('Strong Growth')).toBeDefined(); + }); +}); diff --git a/scratch/ContributionForecast.tsx b/scratch/ContributionForecast.tsx new file mode 100644 index 000000000..077e96d13 --- /dev/null +++ b/scratch/ContributionForecast.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { + TrendingUp, + TrendingDown, + Minus, + Sparkles, + Calendar, + Zap, + LineChart, + Activity, + Target +} from 'lucide-react'; +import type { ActivityData } from '@/types/dashboard'; +import { useTranslation } from '@/context/TranslationContext'; + +interface ContributionForecastProps { + activity: ActivityData[]; + totalContributions?: number; +} + +export default function ContributionForecast({ activity = [], totalContributions }: ContributionForecastProps) { + const { t } = useTranslation(); + + const calculations = useMemo(() => { + if (!activity || activity.length === 0) { + return { + weeklyVelocity: 0, + monthlyVelocity: 0, + projectedMonthEndTotal: totalContributions || 0, + projectedYearEndTotal: totalContributions || 0, + consistencyScore: 0, + consistencyLevel: 'inactive', + slope: 0, + trendType: 'stable', + hasActivity: false, + }; + } + + const N = activity.length; + const currentTotal = totalContributions !== undefined ? totalContributions : activity.reduce((sum, d) => sum + d.count, 0); + + // 1. Average Daily, Weekly, Monthly Velocity + const totalActivityCommits = activity.reduce((sum, d) => sum + d.count, 0); + const avgDaily = totalActivityCommits / N; + const weeklyVelocity = avgDaily * 7; + const monthlyVelocity = avgDaily * 30; + + // 2. Linear Regression (slope m and intercept c) + // x_i = i, y_i = activity[i].count + const meanX = (N - 1) / 2; + const meanY = totalActivityCommits / N; + + let num = 0; + let den = 0; + for (let i = 0; i < N; i++) { + const xDiff = i - meanX; + num += xDiff * (activity[i].count - meanY); + den += xDiff * xDiff; + } + + const slope = den !== 0 ? num / den : 0; + const intercept = meanY - slope * meanX; + + // 3. Date Projections + // Use the date of the last entry in activity as "current date", fallback to new Date() + const lastEntryDateStr = activity[N - 1].date; + let currentDate = new Date(lastEntryDateStr); + if (isNaN(currentDate.getTime())) { + currentDate = new Date(); + } + + // End of Month + const endOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); + const timeDiffMonth = endOfMonth.getTime() - currentDate.getTime(); + const daysRemainingInMonth = Math.max(0, Math.ceil(timeDiffMonth / (1000 * 60 * 60 * 24))); + + // End of Year + const endOfYear = new Date(currentDate.getFullYear(), 11, 31); + const timeDiffYear = endOfYear.getTime() - currentDate.getTime(); + const daysRemainingInYear = Math.max(0, Math.ceil(timeDiffYear / (1000 * 60 * 60 * 24))); + + // Loop to sum predicted future daily commits (guaranteed non-negative) + let projectedMonthExtra = 0; + for (let d = 1; d <= daysRemainingInMonth; d++) { + // day index for future day is N - 1 + d + const projectedDaily = slope * (N - 1 + d) + intercept; + projectedMonthExtra += Math.max(0, projectedDaily); + } + const projectedMonthEndTotal = Math.round(currentTotal + projectedMonthExtra); + + let projectedYearExtra = 0; + for (let d = 1; d <= daysRemainingInYear; d++) { + const projectedDaily = slope * (N - 1 + d) + intercept; + projectedYearExtra += Math.max(0, projectedDaily); + } + const projectedYearEndTotal = Math.round(currentTotal + projectedYearExtra); + + // 4. Consistency Score + // Active days (days with count > 0) + const activeDays = activity.filter(d => d.count > 0).length; + const activeRatio = activeDays / N; + const consistencyScore = Math.min(100, Math.round(activeRatio * 100)); + + let consistencyLevel: 'elite' | 'consistent' | 'occasional' | 'sporadic' | 'inactive' = 'inactive'; + if (consistencyScore >= 85) { + consistencyLevel = 'elite'; + } else if (consistencyScore >= 60) { + consistencyLevel = 'consistent'; + } else if (consistencyScore >= 30) { + consistencyLevel = 'occasional'; + } else if (consistencyScore > 0) { + consistencyLevel = 'sporadic'; + } + + // 5. Trend slope categorization + let trendType: 'strong_growth' | 'moderate_growth' | 'stable' | 'cooling' | 'decline' = 'stable'; + if (slope > 0.02) { + trendType = 'strong_growth'; + } else if (slope > 0.005) { + trendType = 'moderate_growth'; + } else if (slope < -0.02) { + trendType = 'decline'; + } else if (slope < -0.005) { + trendType = 'cooling'; + } + + return { + weeklyVelocity, + monthlyVelocity, + projectedMonthEndTotal, + projectedYearEndTotal, + consistencyScore, + consistencyLevel, + slope, + trendType, + hasActivity: true, + }; + }, [activity, totalContributions]); + + const { + weeklyVelocity, + monthlyVelocity, + projectedMonthEndTotal, + projectedYearEndTotal, + consistencyScore, + consistencyLevel, + slope, + trendType, + hasActivity, + } = calculations; + + const getTrendIcon = () => { + switch (trendType) { + case 'strong_growth': + case 'moderate_growth': + return ; + case 'decline': + case 'cooling': + return ; + default: + return ; + } + }; + + const getTrendColorClass = () => { + switch (trendType) { + case 'strong_growth': + case 'moderate_growth': + return 'text-emerald-600 dark:text-emerald-400'; + case 'decline': + case 'cooling': + return 'text-rose-600 dark:text-rose-400'; + default: + return 'text-zinc-500 dark:text-[#A1A1AA]'; + } + }; + + return ( + +
+
+ +
+

+ {t('dashboard.forecast.title')} +

+

+ {t('dashboard.forecast.subtitle')} +

+
+
+
+ + {!hasActivity ? ( +
+ +

{t('dashboard.forecast.no_activity')}

+
+ ) : ( + <> + {/* Main Metrics Grid */} +
+ +
+ {t('dashboard.forecast.weekly_velocity')} + +
+
+ {t('dashboard.forecast.commits_per_week', { count: weeklyVelocity.toFixed(1) })} +
+
+ + +
+ {t('dashboard.forecast.monthly_velocity')} + +
+
+ {t('dashboard.forecast.commits_per_month', { count: monthlyVelocity.toFixed(1) })} +
+
+ + +
+ {t('dashboard.forecast.projected_month')} + +
+
+ {t('dashboard.forecast.commits', { count: String(projectedMonthEndTotal) })} +
+
+ + +
+ {t('dashboard.forecast.projected_year')} + +
+
+ {t('dashboard.forecast.commits', { count: String(projectedYearEndTotal) })} +
+
+
+ + {/* Details Section */} +
+ {/* Consistency card */} +
+
+ + {t('dashboard.forecast.consistency_rating')} + + + {t(`dashboard.forecast.consistency_level.${consistencyLevel}`)} + +
+
+ +
+
+ {consistencyScore}% active days +
+
+ + {/* Trend card */} +
+
+ + {t('dashboard.forecast.trend_direction')} + + + {t(`dashboard.forecast.trends.${trendType}`)} + + + {t('dashboard.forecast.growth_rate', { rate: slope.toFixed(4) })} + +
+
+ {getTrendIcon()} +
+
+
+ + )} +
+ ); +} diff --git a/scratch/ContributionReplay.test.tsx b/scratch/ContributionReplay.test.tsx new file mode 100644 index 000000000..98f78e52d --- /dev/null +++ b/scratch/ContributionReplay.test.tsx @@ -0,0 +1,207 @@ +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import ContributionReplay from './ContributionReplay'; + +// Mock framer-motion to avoid animation issues in tests +vi.mock('framer-motion', () => ({ + motion: { + div: ({ + children, + className, + ...props + }: { + children: React.ReactNode; + className?: string; + [key: string]: unknown; + }) => { + const safeProps = { ...props }; + delete safeProps.initial; + delete safeProps.whileInView; + delete safeProps.viewport; + delete safeProps.transition; + return ( +
+ {children} +
+ ); + }, + span: ({ children, ...props }: any) => {children}, + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock lucide-react +vi.mock('lucide-react', () => ({ + Play: (props: any) =>
, + Pause: (props: any) =>
, + RotateCcw: (props: any) =>
, + Calendar: (props: any) =>
, + Flame: (props: any) =>
, +})); + +const mockActivity = [ + { date: '2025-01-05', count: 5, intensity: 1 as const }, + { date: '2025-01-10', count: 10, intensity: 4 as const }, // Peak day + { date: '2025-02-15', count: 3, intensity: 2 as const }, + { date: '2025-03-20', count: 8, intensity: 3 as const }, +]; + +describe('ContributionReplay', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + it('renders the header title and description', () => { + render(); + + expect(screen.getByText('Contribution Replay Timeline')).toBeDefined(); + expect(screen.getByText(/Animate your coding journey/)).toBeDefined(); + }); + + it('renders the empty state fallback when no activity data is provided', () => { + render(); + + expect(screen.getByText('No contribution activity data available for replay.')).toBeDefined(); + }); + + it('toggles play/pause state and correctly accumulates commits on tick', () => { + render(); + + // Initial State: Month = January 2025, Commits = 15, Accumulated = 15 + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + + // Click play button + const playBtn = screen.getByLabelText('Play Replay'); + fireEvent.click(playBtn); + + // Play changes to Pause button + expect(screen.getByLabelText('Pause Replay')).toBeDefined(); + + // Tick 1 (1000ms): February 2025 (Monthly commits = 3, Accumulated commits = 18) + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + expect(screen.getAllByText('3')[0]).toBeDefined(); // Monthly Commits + expect(screen.getByText('18')).toBeDefined(); // Accumulated Commits + + // Tick 2 (1000ms): March 2025 (Monthly commits = 8, Accumulated commits = 26) + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('March 2025')[0]).toBeDefined(); + expect(screen.getAllByText('8')[0]).toBeDefined(); + expect(screen.getByText('26')).toBeDefined(); + + // Loop back: Tick 3 (1000ms) loops back to January 2025 + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + + // Pause the replay + const pauseBtn = screen.getByLabelText('Pause Replay'); + fireEvent.click(pauseBtn); + + // Advancing timers should not change active month when paused + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + }); + + it('resets playback index back to 0 on reset click', () => { + render(); + + // Move forward + const playBtn = screen.getByLabelText('Play Replay'); + fireEvent.click(playBtn); + expect(screen.getByLabelText('Pause Replay')).toBeDefined(); + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + + // Reset replay + const resetBtn = screen.getByLabelText('Reset Replay'); + fireEvent.click(resetBtn); + + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + // Timer should also be stopped + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(screen.getAllByText('January 2025')[0]).toBeDefined(); + }); + + it('updates the month on timeline manual scrub slider input change', () => { + render(); + + const scrubSlider = screen.getByLabelText('Scrub timeline months') as HTMLInputElement; + + // Move slider to index 2 (March 2025) + fireEvent.change(scrubSlider, { target: { value: '2' } }); + + expect(screen.getAllByText('March 2025')[0]).toBeDefined(); + expect(screen.getAllByText('8')[0]).toBeDefined(); // Monthly Commits + expect(screen.getByText('26')).toBeDefined(); // Accumulated Commits + }); + + it('updates the month on speed index and speed multiplier change', () => { + render(); + + // Verify default speed label + expect(screen.getByText('1x')).toBeDefined(); + + // Change speed via speed slider to 2x (index 1) + const speedSlider = screen.getByLabelText('Select playback speed multiplier') as HTMLInputElement; + fireEvent.change(speedSlider, { target: { value: '1' } }); + + expect(screen.getByText('2x')).toBeDefined(); + + // Click play and verify tick speed is now 500ms + const playBtn = screen.getByLabelText('Play Replay'); + fireEvent.click(playBtn); + expect(screen.getByLabelText('Pause Replay')).toBeDefined(); + + // Advance by 550ms -> should tick to February 2025 + act(() => { + vi.advanceTimersByTime(550); + }); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + }); + + it('supports keyboard accessibility for controls and navigation', () => { + render(); + + // Controls should have tabIndex 0 and be natively focusable + const playBtn = screen.getByLabelText('Play Replay'); + playBtn.focus(); + expect(document.activeElement).toBe(playBtn); + + const resetBtn = screen.getByLabelText('Reset Replay'); + resetBtn.focus(); + expect(document.activeElement).toBe(resetBtn); + + // Month Navigation list buttons + const monthBtns = screen.getAllByLabelText(/Jump to/); + expect(monthBtns.length).toBeGreaterThan(0); + + // Focus first month jump button + monthBtns[1].focus(); + expect(document.activeElement).toBe(monthBtns[1]); + + // Keyboard click triggering month jump + fireEvent.click(monthBtns[1]); + expect(screen.getAllByText('February 2025')[0]).toBeDefined(); + }); +}); diff --git a/scratch/ContributionReplay.tsx b/scratch/ContributionReplay.tsx new file mode 100644 index 000000000..d315ce05e --- /dev/null +++ b/scratch/ContributionReplay.tsx @@ -0,0 +1,465 @@ +'use client'; + +import { useState, useEffect, useRef, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { Play, Pause, RotateCcw, Calendar, Flame } from 'lucide-react'; +import { useTranslation } from '@/context/TranslationContext'; +import type { ActivityData } from '@/types/dashboard'; + +interface MonthData { + monthKey: string; // "YYYY-MM" + label: string; // short formatted e.g. "Jan '25" + fullLabel: string; // full formatted e.g. "January 2025" + commits: number; + activityItems: ActivityData[]; +} + +export interface ContributionReplayProps { + activity?: ActivityData[]; +} + +export default function ContributionReplay({ activity = [] }: ContributionReplayProps) { + const { t } = useTranslation(); + + // Controls state + const [isPlaying, setIsPlaying] = useState(false); + const [currentMonthIndex, setCurrentMonthIndex] = useState(0); + const [playbackSpeed, setPlaybackSpeed] = useState(1); // 1, 2, 4 + const timerRef = useRef(null); + + // Chronologically sort and process activity data safely + const processedData = useMemo(() => { + const activityList = activity || []; + const sorted = [...activityList].sort((a, b) => a.date.localeCompare(b.date)); + + // Group by month YYYY-MM + const monthsMap = new Map(); + let peakDay = { date: '', count: 0 }; + + sorted.forEach((item) => { + if (!item.date) return; + const key = item.date.substring(0, 7); // "YYYY-MM" + if (!monthsMap.has(key)) { + monthsMap.set(key, { commits: 0, items: [] }); + } + + const group = monthsMap.get(key)!; + group.commits += item.count || 0; + group.items.push(item); + + if ((item.count || 0) > peakDay.count) { + peakDay = { date: item.date, count: item.count }; + } + }); + + const monthsList: MonthData[] = Array.from(monthsMap.entries()).map(([monthKey, data]) => { + let label = monthKey; + let fullLabel = monthKey; + + const parts = monthKey.split('-'); + if (parts.length === 2) { + const year = parts[0]; + const monthNum = parseInt(parts[1], 10); + const monthNames = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + const fullMonthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + const idx = monthNum - 1; + if (idx >= 0 && idx < 12) { + label = `${monthNames[idx]} '${year.substring(2)}`; + fullLabel = `${fullMonthNames[idx]} ${year}`; + } + } + + return { + monthKey, + label, + fullLabel, + commits: data.commits, + activityItems: data.items, + }; + }).sort((a, b) => a.monthKey.localeCompare(b.monthKey)); + + // Calculate cumulative sums + const accumulatedCommits: number[] = []; + let sum = 0; + monthsList.forEach((m) => { + sum += m.commits; + accumulatedCommits.push(sum); + }); + + const maxMonthCommits = monthsList.reduce((max, m) => Math.max(max, m.commits), 0) || 1; + + return { + monthsList, + accumulatedCommits, + peakDay, + maxMonthCommits, + }; + }, [activity]); + + const { monthsList, accumulatedCommits, peakDay, maxMonthCommits } = processedData; + + // Handle auto-playback ticks + useEffect(() => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + + if (isPlaying && monthsList.length > 0) { + const intervalTime = Math.max(100, 1000 / playbackSpeed); + timerRef.current = setInterval(() => { + setCurrentMonthIndex((prevIndex) => { + if (prevIndex >= monthsList.length - 1) { + return 0; // Loop back + } + return prevIndex + 1; + }); + }, intervalTime); + } + + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, [isPlaying, playbackSpeed, monthsList.length]); + + // Clean up playback on component unmount + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + + const activeMonth = monthsList[currentMonthIndex] || monthsList[0]; + const accumulatedSoFar = accumulatedCommits[currentMonthIndex] ?? 0; + + // Active month statistics breakdown (low, medium, high intensity) + const activeMonthBreakdown = useMemo(() => { + let low = 0; + let medium = 0; + let high = 0; + if (!activeMonth || !activeMonth.activityItems) { + return { low, medium, high }; + } + activeMonth.activityItems.forEach((d) => { + if (d.count > 0) { + if (d.intensity <= 1) { + low++; + } else if (d.intensity <= 3) { + medium++; + } else { + high++; + } + } + }); + return { low, medium, high }; + }, [activeMonth]); + + // Empty state boundary check + if (monthsList.length === 0) { + return ( +
+ +

+ {t('dashboard.replay.title')} +

+

+ {t('dashboard.replay.no_activity')} +

+
+ ); + } + + const speedOptions = [1, 2, 4]; + const speedIndex = speedOptions.indexOf(playbackSpeed); + + const handleReset = () => { + setIsPlaying(false); + setCurrentMonthIndex(0); + }; + + const togglePlay = () => { + setIsPlaying(!isPlaying); + }; + + return ( + + {/* Header */} +
+
+

+ + {t('dashboard.replay.title')} +

+

+ {t('dashboard.replay.subtitle')} +

+
+ + {/* Peak Activity Indicator */} + {peakDay.count > 0 && ( +
+ + {t('dashboard.replay.peak_activity')}: + + {t('dashboard.replay.peak_commits', { count: peakDay.count.toString() })}{' '} + {t('dashboard.replay.peak_date', { date: peakDay.date })} + +
+ )} +
+ + {/* Main stats visual dashboard */} +
+
+

+ {t('dashboard.replay.active_month')} +

+

+ {activeMonth.fullLabel} +

+
+ +
+

+ {t('dashboard.replay.monthly_total')} +

+

+ {activeMonth.commits} +

+
+ +
+

+ {t('dashboard.replay.accumulated')} +

+

+ {accumulatedSoFar} +

+
+
+ + {/* Interactive Visualizations */} +
+ {/* Month Calendar Mini Grid */} +
+
+

+ {activeMonth.fullLabel} — Daily Rhythm +

+ + {activeMonth.activityItems.length === 0 ? ( +

+ {t('dashboard.replay.no_activity')} +

+ ) : ( +
+ {activeMonth.activityItems.map((day, idx) => { + let colorClass = 'bg-neutral-100 dark:bg-neutral-800/50 border border-transparent'; + if (day.count > 0) { + if (day.intensity <= 1) { + colorClass = 'bg-indigo-500/20 border border-indigo-500/10 text-indigo-400'; + } else if (day.intensity <= 3) { + colorClass = 'bg-indigo-500/50 border border-indigo-500/20 text-indigo-300'; + } else { + colorClass = 'bg-indigo-500 border border-indigo-400 text-white shadow-[0_0_8px_rgba(99,102,241,0.4)]'; + } + } + + return ( +
+ {day.count > 0 ? day.count : ''} +
+ ); + })} +
+ )} +
+ + {/* Intensity counts breakdown */} +
+
+ + {t('dashboard.replay.intensity_low')}: {activeMonthBreakdown.low} +
+
+ + {t('dashboard.replay.intensity_medium')}: {activeMonthBreakdown.medium} +
+
+ + {t('dashboard.replay.intensity_high')}: {activeMonthBreakdown.high} +
+
+
+ + {/* Monthly Histogram Chart */} +
+
+

+ {t('dashboard.replay.monthly_total')} Breakdown +

+ +
+ {monthsList.map((m, idx) => { + const heightPercentage = Math.max(10, Math.min(100, (m.commits / maxMonthCommits) * 100)); + const isActive = idx === currentMonthIndex; + const isPassed = idx <= currentMonthIndex; + + return ( + + ); + })} +
+
+ +
+ {monthsList[0]?.label} + {monthsList[monthsList.length - 1]?.label} +
+
+
+ + {/* Playback Controls & Timeline Slider */} +
+ {/* Manual Timeline Scrub Slider */} +
+
+ {activeMonth.fullLabel} + {currentMonthIndex + 1} / {monthsList.length} +
+ setCurrentMonthIndex(parseInt(e.target.value, 10))} + aria-label={t('dashboard.replay.aria_scrub')} + className="w-full accent-indigo-500 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none h-2 cursor-pointer focus-visible:ring-2 focus-visible:ring-indigo-500 outline-none" + /> +
+ + {/* Buttons and Speed controls panel */} +
+
+ + + +
+ + {/* Speed Multiplier controls */} +
+ + { + const idx = parseInt(e.target.value, 10); + setPlaybackSpeed(speedOptions[idx]); + }} + aria-labelledby="speed-label" + aria-label={t('dashboard.replay.aria_speed')} + className="w-24 accent-indigo-500 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none h-1.5 cursor-pointer focus-visible:ring-2 focus-visible:ring-indigo-500 outline-none" + /> + + {t('dashboard.replay.speed_value', { speed: playbackSpeed.toString() })} + +
+
+
+ + {/* Horizontal Month Navigation Bar */} +
+ {monthsList.map((m, idx) => { + const isActive = idx === currentMonthIndex; + return ( + + ); + })} +
+
+ ); +} diff --git a/scratch/DashboardClient.tsx b/scratch/DashboardClient.tsx new file mode 100644 index 000000000..6a90bb22b --- /dev/null +++ b/scratch/DashboardClient.tsx @@ -0,0 +1,1324 @@ +'use client'; + +import { useState, useEffect, useRef, useCallback, useSyncExternalStore } from 'react'; +import { createPortal } from 'react-dom'; +import { AnimatePresence, motion } from 'framer-motion'; +import DashboardSkeleton from './DashboardSkeleton'; +import { X, RefreshCw, Share2, Network } from 'lucide-react'; +import Link from 'next/link'; +import { toast } from 'sonner'; +import type { + Achievement, + Repository, + HallOfFameAward, + RepoActivityInfo, + DeploymentData, +} from '@/types/dashboard'; +import type { GraphNode, GraphLink } from '@/types'; + +import RefreshButton from './RefreshButton'; +import ProfileCard from './ProfileCard'; +import Achievements from './Achievements'; +import ActivityLandscape from './ActivityLandscape'; +import LanguageChart from './LanguageChart'; +import CommitClock from './CommitClock'; +import Heatmap from './Heatmap'; +import HistoricalTrendView from './HistoricalTrendView'; +import AIInsights from './AIInsights'; +import StatsCard from './StatsCard'; +import RepositoryGraph from './RepositoryGraph'; +import HallOfFame from './HallOfFame'; +import ComparisonStatsCard from './ComparisonStatsCard'; +import RadarChart from './RadarChart'; +import GrowthTrendChart from './GrowthTrendChart'; +import { useRouter } from 'next/navigation'; +import ProfileOptimizerModal from './ProfileOptimizerModal'; +import ResumeProfileSection from './ResumeProfileSection'; +import type { DashboardPeriod } from '@/utils/dashboardPeriod'; +import { PopularRepos } from './PopularPinnnedRepos'; +import InactiveRepoReminder from './InactiveRepoReminder'; +import PRInsightsClient from './PRInsights/PRInsightsClient'; +import CIAnalyticsClient from './CIAnalytics/CIAnalyticsClient'; +import DeploymentTracker from './DeploymentTracker'; +import ArchitectureVisualizer from './ArchitectureVisualizer'; +import RepositoryImpactAnalyzer from './RepositoryImpactAnalyzer'; +import ContributionForecast from './ContributionForecast'; +import ProfileComparisonAnalytics from './ProfileComparisonAnalytics'; +import ContributionReplay from './ContributionReplay'; + +// Define the dashboard data structure +export interface DashboardData { + profile: { + username: string; + name: string; + avatarUrl: string; + isPro: boolean; + bio: string; + location: string; + joinedDate: string; + developerScore: number; + stats: { + repositories: number; + followers: number; + following: number; + stars: number; + }; + }; + stats: { + currentStreak: number; + peakStreak: number; + totalContributions: number; + }; + languages: Array<{ + name: string; + color: string; + percentage: number; + }>; + activity: Array<{ + date: string; + count: number; + intensity: 0 | 1 | 2 | 3 | 4; + }>; + insights: Array<{ + id: string; + icon: string; + text: string; + }>; + achievements: Achievement[]; + commitClock: Array<{ + day: string; + commits: number; + }>; + graphData: { + nodes: GraphNode[]; + links: GraphLink[]; + }; + popularRepos?: Repository[]; + pinnedRepos?: Repository[]; + starredRepos?: Repository[]; + deployments?: DeploymentData[]; + hallOfFame?: HallOfFameAward[]; +} + +interface DashboardClientProps { + initialData: DashboardData; + allRepoActivity?: RepoActivityInfo[]; + username: string; + compareData?: DashboardData | null; + period: DashboardPeriod; +} + +export interface ProfileMetrics { + currentStreak: number; + commitClock: { day: string; commits: number }[]; // e.g., Sun-Sat daily totals +} + +export interface CoderProfile { + peakHourStart: number; + peakHourEnd: number; + profileName: 'Early Builder ☀' | 'Weekend Warrior 🚀' | 'Consistent Runner 🏃‍♂️'; + hourlyDistribution: number[]; + activeWeekdays: string[]; +} + +/** + * Generates a coder profile based on available metrics. + */ +export function generateCoderProfile(metrics: ProfileMetrics): CoderProfile { + const { currentStreak, commitClock } = metrics; + + let profileName: CoderProfile['profileName'] = 'Early Builder ☀'; + + // 1. Analyze Commit Clock for Weekend Warrior + let isWeekendWarrior = false; + if (commitClock && commitClock.length === 7) { + const weekendCommits = commitClock[0].commits + commitClock[6].commits; // Sunday(0) + Saturday(6) + const totalCommits = commitClock.reduce((sum, d) => sum + d.commits, 0); + if (totalCommits > 0 && weekendCommits / totalCommits > 0.35) { + isWeekendWarrior = true; + } + } + + // 2. Determine Final Profile Type + if (currentStreak >= 10) { + profileName = 'Consistent Runner 🏃‍♂️'; + } else if (isWeekendWarrior) { + profileName = 'Weekend Warrior 🚀'; + } + + // 3. Populate UI properties based on the derived profile. + let peakHourStart = 9; + let peakHourEnd = 17; + let hourlyDistribution = new Array(24).fill(0); + let activeWeekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']; + + if (profileName === 'Early Builder ☀') { + peakHourStart = 6; + peakHourEnd = 10; + hourlyDistribution = Array.from({ length: 24 }, (_, h) => { + const distFromEight = Math.abs(h - 8); + return Math.max(6, Math.round(100 - distFromEight * 10)); + }); + activeWeekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']; + } else if (profileName === 'Weekend Warrior 🚀') { + peakHourStart = 10; + peakHourEnd = 16; + hourlyDistribution = Array.from({ length: 24 }, (_, h) => { + const distFromMidday = Math.abs(h - 13); + return Math.max(8, Math.round(100 - distFromMidday * 8.5)); + }); + activeWeekdays = ['Sat', 'Sun']; + } else if (profileName === 'Consistent Runner 🏃‍♂️') { + peakHourStart = 13; + peakHourEnd = 17; + hourlyDistribution = Array.from({ length: 24 }, (_, h) => { + const distFromThree = Math.abs(h - 15); + return Math.max(12, Math.round(100 - distFromThree * 7)); + }); + activeWeekdays = ['Mon', 'Wed', 'Fri', 'Sat']; + } + + return { + peakHourStart, + peakHourEnd, + profileName, + hourlyDistribution, + activeWeekdays, + }; +} + +function calculateInactivityGaps(activity: Array<{ date: string; count: number }>) { + let currentGap = 0; + let longestGap = 0; + + activity.forEach((day) => { + if (day.count === 0) { + currentGap++; + } else { + if (currentGap > longestGap) { + longestGap = currentGap; + } + currentGap = 0; + } + }); + + if (currentGap > longestGap) { + longestGap = currentGap; + } + + return longestGap; +} + +function calculateComebackStreak(activity: Array<{ date: string; count: number }>) { + let currentGap = 0; + let longestGap = 0; + let longestGapEndIndex = -1; + + activity.forEach((day, index) => { + if (day.count === 0) { + currentGap++; + } else { + if (currentGap > longestGap) { + longestGap = currentGap; + longestGapEndIndex = index; + } + currentGap = 0; + } + }); + + if (longestGapEndIndex === -1 || longestGap === 0) { + return 0; + } + + let comebackStreak = 0; + for (let i = longestGapEndIndex; i < activity.length; i++) { + if (activity[i].count > 0) { + comebackStreak++; + } else { + break; + } + } + return comebackStreak; +} + +function calculateRecoverySpeed(activity: Array<{ date: string; count: number }>) { + let currentGap = 0; + let longestGap = 0; + let longestGapEndIndex = -1; + + activity.forEach((day, index) => { + if (day.count === 0) { + currentGap++; + } else { + if (currentGap > longestGap) { + longestGap = currentGap; + longestGapEndIndex = index; + } + currentGap = 0; + } + }); + + if (longestGapEndIndex === -1 || longestGap === 0) { + return 0; + } + + let sum = 0; + let count = 0; + for (let i = longestGapEndIndex; i < Math.min(activity.length, longestGapEndIndex + 7); i++) { + sum += activity[i].count; + count++; + } + + return count > 0 ? Math.round((sum / count) * 10) / 10 : 0; +} + +function getPersonalityTags( + profile: DashboardData['profile'], + stats: DashboardData['stats'], + languages: DashboardData['languages'], + coderProfile: CoderProfile +): string[] { + const tags: string[] = []; + + if (stats.peakStreak >= 30) { + tags.push('Consistency Beast 🔥'); + } else if (stats.peakStreak >= 15) { + tags.push('Streak Runner ⚡'); + } + + if (stats.totalContributions >= 500) { + tags.push('Hardcore Committer 💻'); + } else if (stats.totalContributions >= 200) { + tags.push('Active Committer 🛠️'); + } + + if (profile.stats.repositories >= 25) { + tags.push('Open Source Explorer 🚀'); + } + + const hasFrontend = languages.some((l) => + ['TypeScript', 'JavaScript', 'HTML', 'CSS'].includes(l.name) + ); + const hasBackend = languages.some((l) => + ['Python', 'Java', 'Go', 'Rust', 'C++', 'Ruby', 'PHP', 'C#'].includes(l.name) + ); + + if (hasFrontend && hasBackend) { + tags.push('Full Stack Builder 🧠'); + } else if (hasFrontend) { + tags.push('UI Craftsman 🎨'); + } else if (hasBackend) { + tags.push('Backend Architect ⚙️'); + } + + if (coderProfile.profileName === 'Early Builder ☀') { + tags.push('Early Builder ☀'); + } else if (coderProfile.profileName === 'Weekend Warrior 🚀') { + tags.push('Weekend Warrior 🚀'); + } else if (coderProfile.profileName === 'Consistent Runner 🏃‍♂️') { + tags.push('Consistent Runner 🏃‍♂️'); + } + + return tags.slice(0, 3); +} + +export default function DashboardClient({ + initialData, + allRepoActivity = [], + username, + compareData = null, + period, +}: DashboardClientProps) { + const isLoading = useSyncExternalStore( + () => () => {}, + () => false, + () => true + ); + const [secondUserData, setSecondUserData] = useState(compareData); + const [activeTab, setActiveTab] = useState<'overview' | 'pr-insights' | 'ci-analytics'>( + 'overview' + ); + const [isCompareMode, setIsCompareMode] = useState(Boolean(compareData)); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isOptimizerOpen, setIsOptimizerOpen] = useState(false); + const [isVisualizerOpen, setIsVisualizerOpen] = useState(false); + const [secondUsernameInput, setSecondUsernameInput] = useState(''); + const [isLoadingSecond, setIsLoadingSecond] = useState(false); + const [compareError, setCompareError] = useState(null); + const router = useRouter(); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + + const modalRef = useRef(null); + const compareInputRef = useRef(null); + const triggerRef = useRef(null); + + const allRepos = [ + ...(initialData.popularRepos || []), + ...(initialData.pinnedRepos || []), + ...(initialData.starredRepos || []), + ]; + const deduplicatedRepos = Array.from( + new Map(allRepos.map((repo) => [repo.name, repo])).values() + ); + + const compareUser1 = { + profile: initialData.profile, + stats: initialData.stats, + languages: initialData.languages, + activity: initialData.activity, + achievements: initialData.achievements, + popularRepos: deduplicatedRepos, + }; + + const compareUser2 = secondUserData ? { + profile: secondUserData.profile, + stats: secondUserData.stats, + languages: secondUserData.languages, + activity: secondUserData.activity, + achievements: secondUserData.achievements, + popularRepos: [ + ...(secondUserData.popularRepos || []), + ...(secondUserData.pinnedRepos || []), + ...(secondUserData.starredRepos || []), + ].reduce((acc: Repository[], repo) => { + if (!acc.some((r) => r.name === repo.name)) { + acc.push(repo); + } + return acc; + }, []), + } : null; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setIsModalOpen(false); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const handleModalKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key !== 'Tab' || !modalRef.current) return; + + const focusableElements = modalRef.current.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]' + ); + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + const activeEl = document.activeElement; + const isFocusInModal = modalRef.current.contains(activeEl); + + if (!isFocusInModal) { + e.preventDefault(); + firstElement.focus(); + return; + } + + if (e.shiftKey) { + if (activeEl === firstElement) { + e.preventDefault(); + lastElement.focus(); + } + } else { + if (activeEl === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + }, []); + + useEffect(() => { + if (!isModalOpen) { + triggerRef.current?.focus(); + } + }, [isModalOpen]); + + const handleModalAnimationComplete = useCallback(() => { + compareInputRef.current?.focus(); + }, []); + + const handleOpenModal = () => { + setSecondUsernameInput(''); + setCompareError(null); + setIsModalOpen(true); + }; + + const handleFetchComparison = async (e: React.FormEvent) => { + e.preventDefault(); + const query = secondUsernameInput.trim(); + if (!query) return; + + if (query.toLowerCase() === username.toLowerCase()) { + setCompareError('Cannot compare a profile with itself.'); + return; + } + + setIsLoadingSecond(true); + setCompareError(null); + + try { + const res = await fetch(`/api/github?username=${encodeURIComponent(query)}`); + if (!res.ok) { + if (res.status === 404) { + throw new Error(`GitHub user "${query}" not found`); + } + const errData = await res.json().catch(() => ({})); + throw new Error(errData.error || `Failed to fetch data (status ${res.status})`); + } + + const data = await res.json(); + setSecondUserData(data); + setIsCompareMode(true); + + router.replace(`/dashboard/${username}?compare=${data.profile.username}`); + + setIsModalOpen(false); + toast.success(`Comparing ${username} vs ${data.profile.username}`); + } catch (err: unknown) { + const errMessage = + err instanceof Error ? err.message : 'An error occurred while fetching profile data.'; + setCompareError(errMessage); + } finally { + setIsLoadingSecond(false); + } + }; + + const handleExitCompare = () => { + setIsCompareMode(false); + setSecondUserData(null); + + router.replace(`/dashboard/${username}`); + + toast.info('Returned to single profile view'); + }; + + const handleShareComparison = async () => { + if (!secondUserData) return; + + const compareUrl = `${window.location.origin}/dashboard/${username}?compare=${secondUserData.profile.username}`; + + try { + if (navigator.share) { + await navigator.share({ + title: `${username} vs ${secondUserData.profile.username}`, + text: 'Check out this GitHub profile comparison', + url: compareUrl, + }); + } else { + await navigator.clipboard.writeText(compareUrl); + toast.success('Comparison link copied!'); + } + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + return; + } + + toast.error('Failed to share comparison link'); + } + }; + + const handleShareDashboard = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + toast.success('Link copied to clipboard!'); + } catch { + toast.error('Failed to copy dashboard link'); + } + }; + + const coderProfileA = generateCoderProfile({ + currentStreak: initialData.stats.currentStreak, + commitClock: initialData.commitClock, + }); + + const coderProfileB = secondUserData + ? generateCoderProfile({ + currentStreak: secondUserData.stats.currentStreak, + commitClock: secondUserData.commitClock, + }) + : null; + const gapA = calculateInactivityGaps(initialData.activity); + const gapB = secondUserData ? calculateInactivityGaps(secondUserData.activity) : 0; + + const recoveryA = calculateRecoverySpeed(initialData.activity); + const recoveryB = secondUserData ? calculateRecoverySpeed(secondUserData.activity) : 0; + + const comebackA = calculateComebackStreak(initialData.activity); + const comebackB = secondUserData ? calculateComebackStreak(secondUserData.activity) : 0; + + const badgesA: string[] = []; + const badgesB: string[] = []; + + const personalityTagsA = getPersonalityTags( + initialData.profile, + initialData.stats, + initialData.languages, + coderProfileA + ); + const personalityTagsB = + secondUserData && coderProfileB + ? getPersonalityTags( + secondUserData.profile, + secondUserData.stats, + secondUserData.languages, + coderProfileB + ) + : []; + + if (isCompareMode && secondUserData) { + if (initialData.stats.peakStreak > secondUserData.stats.peakStreak) { + badgesA.push('Most Consistent'); + } else if (secondUserData.stats.peakStreak > initialData.stats.peakStreak) { + badgesB.push('Most Consistent'); + } + + if (initialData.stats.totalContributions > secondUserData.stats.totalContributions) { + badgesA.push('Highest Activity'); + } else if (secondUserData.stats.totalContributions > initialData.stats.totalContributions) { + badgesB.push('Highest Activity'); + } + + if (initialData.stats.currentStreak > secondUserData.stats.currentStreak) { + badgesA.push('Strongest Streak'); + } else if (secondUserData.stats.currentStreak > initialData.stats.currentStreak) { + badgesB.push('Strongest Streak'); + } + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ {isCompareMode && secondUserData && ( + + )} +
+
+ {!isCompareMode && ( + <> + + + + 🏆 Achievements + + + + )} + {isCompareMode && secondUserData && ( + + )} + + + + + + + + Generate Your Own + +
+
+ +
+
+ + + +
+
+ + {activeTab === 'ci-analytics' ? ( + + ) : activeTab === 'pr-insights' ? ( + + ) : !isCompareMode || !secondUserData || !coderProfileB ? ( +
+ + +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ + +
+
+ ) : ( +
+ {/* Compare profile code blocks preserve standard layouts */} +
+
+ +
+ {personalityTagsA.map((t) => ( + + {t} + + ))} +
+
+ +
+ +
+ {personalityTagsB.map((t) => ( + + {t} + + ))} +
+
+
+ + + +
+

+ Head-to-Head Stats +

+
+ + + + + + + +
+
+ +
+ +

+ ⏰ Peak Coding Time Analysis +

+
+
+

+ {initialData.profile.name} +

+ + {coderProfileA.profileName} + +
+

+ Peak Hours:{' '} + + {coderProfileA.peakHourStart}:00 - {coderProfileA.peakHourEnd}:00 + +

+

+ Active Days:{' '} + + {coderProfileA.activeWeekdays.join(', ')} + +

+
+
+ {coderProfileA.hourlyDistribution.map((v, h) => ( +
+ ))} +
+
+ +
+

+ {secondUserData.profile.name} +

+ + {coderProfileB.profileName} + +
+

+ Peak Hours:{' '} + + {coderProfileB.peakHourStart}:00 - {coderProfileB.peakHourEnd}:00 + +

+

+ Active Days:{' '} + + {coderProfileB.activeWeekdays.join(', ')} + +

+
+
+ {coderProfileB.hourlyDistribution.map((v, h) => ( +
+ ))} +
+
+
+ + + +

+ 💀 Inactivity & Recovery Insights +

+
+
+
+ + Longest Inactive Period + + Lower is Better +
+
+
+ {initialData.profile.name}: {gapA} days {gapA < gapB && '🏆'} +
+
+ {gapB} days {gapB < gapA && '🏆'} : {secondUserData.profile.name} +
+
+
+ +
+
+ + Consistency Recovery Speed + + Avg commits/day after gap +
+
+
recoveryB + ? 'text-emerald-500 dark:text-emerald-400' + : 'text-gray-900 dark:text-white' + } + > + {initialData.profile.name}: {recoveryA} c/d {recoveryA > recoveryB && '🏆'} +
+
recoveryA + ? 'text-emerald-500 dark:text-emerald-400 text-right' + : 'text-gray-900 dark:text-white text-right' + } + > + {recoveryB} c/d {recoveryB > recoveryA && '🏆'} :{' '} + {secondUserData.profile.name} +
+
+
+ +
+
+ + Comeback Streak length + + + First streak after longest gap + +
+
+
comebackB + ? 'text-emerald-500 dark:text-emerald-400' + : 'text-gray-900 dark:text-white' + } + > + {initialData.profile.name}: {comebackA} days {comebackA > comebackB && '🏆'} +
+
comebackA + ? 'text-emerald-500 dark:text-emerald-400 text-right' + : 'text-gray-900 dark:text-white text-right' + } + > + {comebackB} days {comebackB > comebackA && '🏆'} :{' '} + {secondUserData.profile.name} +
+
+
+
+
+
+ +
+ +
+ +
+ +
+ +
+
+

+ {initialData.profile.name}'s Top Languages +

+ +
+
+

+ {secondUserData.profile.name}'s Top Languages +

+ +
+
+ +
+
+

+ {initialData.profile.name}'s Commit Clock +

+ +
+
+

+ {secondUserData.profile.name}'s Commit Clock +

+ +
+
+ +
+
+

+ Commit Activity Comparison +

+
+
+
+

+ {initialData.profile.name}'s Heatmap +

+ +
+
+

+ {secondUserData.profile.name}'s Heatmap +

+ +
+
+
+
+ )} + + + {isModalOpen && ( +
+ setIsModalOpen(false)} + className="absolute inset-0 bg-black/60 backdrop-blur-md" + aria-hidden="true" + /> + + + + +
+

+ Compare Profile +

+

+ Enter another GitHub username to load their statistics and compare them + side-by-side with {initialData.profile.name}. +

+
+ +
+
+ setSecondUsernameInput(e.target.value)} + className="w-full rounded-xl border border-black/10 bg-gray-100 px-4 py-3 text-sm text-black outline-none transition-all duration-200 placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent dark:border-[rgba(255,255,255,0.08)] dark:bg-[#111] dark:text-white dark:placeholder:text-white/65" + /> + {secondUsernameInput.length > 0 && !isLoadingSecond && ( + + )} +
+ + {compareError && ( +

+ ⚠️ {compareError} +

+ )} + + +
+
+
+ )} +
+ + setIsOptimizerOpen(false)} + userData={initialData} + /> + + {typeof window !== 'undefined' && + createPortal( + + {isVisualizerOpen && ( +
+ setIsVisualizerOpen(false)} + className="absolute inset-0 bg-black/60 backdrop-blur-md" + aria-hidden="true" + /> + +
+ setIsVisualizerOpen(false)} /> +
+
+
+ )} +
, + document.body + )} +
+ ); +} diff --git a/scratch/ProfileComparisonAnalytics.test.tsx b/scratch/ProfileComparisonAnalytics.test.tsx new file mode 100644 index 000000000..dbe092c61 --- /dev/null +++ b/scratch/ProfileComparisonAnalytics.test.tsx @@ -0,0 +1,257 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import ProfileComparisonAnalytics, { CompareUser } from './ProfileComparisonAnalytics'; + +// Mock TranslationContext +vi.mock('@/context/TranslationContext', () => ({ + useTranslation: () => ({ + t: (key: string, options?: any) => { + if (options && options.count !== undefined) { + return key; // Simple mock returning key + } + return key; + } + }) +})); + +// Mock framer-motion to render clean HTML containers for testing +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className, style, ...props }: any) => { + const { initial, animate, whileInView, viewport, transition, ...rest } = props; + return ( +
+ {children} +
+ ); + }, + }, +})); + +// Mock lucide-react icons +vi.mock('lucide-react', () => ({ + Award: () => , + Trophy: () => , + Flame: () => , + Sparkles: () => , + Star: () => , + GitFork: () => , + GitCommit: () => , + MapPin: () => , + Calendar: () => , + Check: () => , + Lock: () => , + ExternalLink: () => , + ChevronDown: () => , + ChevronUp: () => , +})); + +const mockUser1: CompareUser = { + profile: { + username: 'alice', + name: 'Alice Cooper', + avatarUrl: 'https://avatars.githubusercontent.com/u/1', + isPro: true, + bio: 'Frontend enthusiast', + location: 'New York', + joinedDate: 'Joined Oct 2020', + developerScore: 85, + stats: { repositories: 12, followers: 150, following: 80, stars: 300 } + }, + stats: { + currentStreak: 5, + peakStreak: 15, + totalContributions: 500 + }, + languages: [ + { name: 'TypeScript', color: '#3178c6', percentage: 70 }, + { name: 'JavaScript', color: '#f1e05a', percentage: 30 } + ], + activity: [ + { date: '2026-05-10', count: 5, intensity: 2 }, + { date: '2026-05-11', count: 10, intensity: 3 }, + { date: '2026-06-01', count: 2, intensity: 1 } + ], + achievements: [ + { id: 'streak-10', title: '10 Day Streak', description: 'Coded for 10 days', icon: 'flame', isUnlocked: true, type: 'streak', threshold: 10, currentValue: 10, progress: 100 }, + { id: 'contrib-100', title: 'Centurion', description: '100 contributions', icon: 'trophy', isUnlocked: true, type: 'contributions', threshold: 100, currentValue: 100, progress: 100 }, + { id: 'ach-3', title: 'Achievement 3', description: 'Description 3', icon: 'star', isUnlocked: true, type: 'streak', threshold: 5, currentValue: 5, progress: 100 }, + { id: 'ach-4', title: 'Achievement 4', description: 'Description 4', icon: 'star', isUnlocked: true, type: 'streak', threshold: 5, currentValue: 5, progress: 100 }, + { id: 'ach-5', title: 'Achievement 5', description: 'Description 5', icon: 'star', isUnlocked: true, type: 'streak', threshold: 5, currentValue: 5, progress: 100 } + ], + popularRepos: [ + { name: 'alice-cool-web', description: 'Alice web project', stargazerCount: 50, forkCount: 10, url: 'https://github.com/alice/alice-cool-web', primaryLanguage: { name: 'TypeScript', color: '#3178c6' }, commits: 20 } as any, + { name: 'alice-utils', description: 'Alice util project', stargazerCount: 10, forkCount: 2, url: 'https://github.com/alice/alice-utils', primaryLanguage: { name: 'JavaScript', color: '#f1e05a' }, commits: 5 } as any + ] +}; + +const mockUser2: CompareUser = { + profile: { + username: 'bob', + name: 'Bob Builder', + avatarUrl: 'https://avatars.githubusercontent.com/u/2', + isPro: false, + bio: 'Backend architect', + location: 'Berlin', + joinedDate: 'Joined Jan 2021', + developerScore: 90, + stats: { repositories: 8, followers: 90, following: 40, stars: 120 } + }, + stats: { + currentStreak: 2, + peakStreak: 8, + totalContributions: 320 + }, + languages: [ + { name: 'TypeScript', color: '#3178c6', percentage: 40 }, + { name: 'Go', color: '#00add8', percentage: 60 } + ], + activity: [ + { date: '2026-05-10', count: 3, intensity: 1 }, + { date: '2026-06-01', count: 8, intensity: 3 } + ], + achievements: [ + { id: 'streak-10', title: '10 Day Streak', description: 'Coded for 10 days', icon: 'flame', isUnlocked: false, type: 'streak', threshold: 10, currentValue: 4, progress: 40 }, + { id: 'contrib-100', title: 'Centurion', description: '100 contributions', icon: 'trophy', isUnlocked: true, type: 'contributions', threshold: 100, currentValue: 100, progress: 100 } + ], + popularRepos: [ + { name: 'bob-go-engine', description: 'Bob high performance engine', stargazerCount: 100, forkCount: 30, url: 'https://github.com/bob/bob-go-engine', primaryLanguage: { name: 'Go', color: '#00add8' }, commits: 40 } as any, + { name: 'bob-script', description: 'Bob basic script', stargazerCount: 5, forkCount: 0, url: 'https://github.com/bob/bob-script', primaryLanguage: { name: 'Go', color: '#00add8' }, commits: 2 } as any + ] +}; + +describe('ProfileComparisonAnalytics', () => { + it('renders usernames, developer scores, and key stats in side-by-side layout', () => { + render(); + + // Check profiles + expect(screen.getByText('Alice Cooper')).toBeDefined(); + expect(screen.getAllByText('@alice')[0]).toBeDefined(); + expect(screen.getByText('Bob Builder')).toBeDefined(); + expect(screen.getAllByText('@bob')[0]).toBeDefined(); + + // Check developer score display + expect(screen.getByText('dashboard.compare.developer_score: 85')).toBeDefined(); + expect(screen.getByText('dashboard.compare.developer_score: 90')).toBeDefined(); + + // Check total contributions + expect(screen.getAllByText('dashboard.compare.contribution_comparison.user_total')[0]).toBeDefined(); + }); + + it('correctly ranks repositories and calculates impact scores', () => { + render(); + + // Scores check: + // bob-go-engine: 40 commits * 3 + 100 stars * 5 + 30 forks * 10 = 920 + // alice-cool-web: 20 commits * 3 + 50 stars * 5 + 10 forks * 10 = 410 + // alice-utils: 5 commits * 3 + 10 stars * 5 + 2 forks * 10 = 85 + // bob-script: 2 commits * 3 + 5 stars * 5 + 0 forks * 10 = 31 + + // Renders repository names + expect(screen.getByText('bob-go-engine')).toBeDefined(); + expect(screen.getByText('alice-cool-web')).toBeDefined(); + expect(screen.getByText('alice-utils')).toBeDefined(); + expect(screen.getByText('bob-script')).toBeDefined(); + + // Check impact score renders + expect(screen.getByText('920')).toBeDefined(); + expect(screen.getByText('410')).toBeDefined(); + expect(screen.getByText('85')).toBeDefined(); + expect(screen.getByText('31')).toBeDefined(); + }); + + it('displays language side-by-side percentage comparisons correctly', () => { + render(); + + // Languages: TS (Alice 70%, Bob 40%), JS (Alice 30%, Bob 0%), Go (Alice 0%, Bob 60%) + expect(screen.getByText('70.0%')).toBeDefined(); + expect(screen.getByText('40.0%')).toBeDefined(); + expect(screen.getByText('30.0%')).toBeDefined(); + expect(screen.getByText('60.0%')).toBeDefined(); + }); + + it('renders achievements status with unlock/lock badges correctly', () => { + render(); + + // We have "10 Day Streak" which is unlocked for Alice but locked for Bob + expect(screen.getByText('10 Day Streak')).toBeDefined(); + + // Alice unlocked badge, Bob locked badge + const aliceUnlockedBadges = screen.getAllByRole('img', { name: 'alice unlocked 10 Day Streak' }); + const bobLockedBadges = screen.getAllByRole('img', { name: 'bob locked 10 Day Streak' }); + + expect(aliceUnlockedBadges.length).toBe(1); + expect(bobLockedBadges.length).toBe(1); + }); + + it('handles zero-activity and empty-state edge cases gracefully', () => { + const emptyUser1: CompareUser = { + profile: { + username: 'empty1', + name: 'Empty One', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, + stats: { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, + languages: [], + activity: [], + achievements: [], + popularRepos: [] + }; + + const emptyUser2: CompareUser = { + profile: { + username: 'empty2', + name: 'Empty Two', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, + stats: { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, + languages: [], + activity: [], + achievements: [], + popularRepos: [] + }; + + render(); + + // Verify empty state fallback messages are rendered + expect(screen.getByText('dashboard.compare.contribution_comparison.no_data')).toBeDefined(); + expect(screen.getByText('dashboard.compare.language_comparison.no_data')).toBeDefined(); + expect(screen.getByText('dashboard.compare.achievement_comparison.no_data')).toBeDefined(); + expect(screen.getByText('dashboard.compare.repository_comparison.no_data')).toBeDefined(); + }); + + it('supports keyboard accessibility controls and ARIA compliance', () => { + const { container } = render(); + + // Region has comparison aria label + const region = screen.getByRole('region', { name: 'dashboard.compare.title' }); + expect(region).toBeDefined(); + + // Verify that links have accessible labels + const repoLinks = screen.getAllByRole('link'); + expect(repoLinks.length).toBeGreaterThanOrEqual(2); + expect(repoLinks[0].getAttribute('aria-label')).toContain('repository on GitHub'); + + // Achievement show more/less toggle button is keyboard-focusable + const toggleButton = screen.getByRole('button', { name: /dashboard.achievements.see_all/i }); + expect(toggleButton).toBeDefined(); + expect(toggleButton.tabIndex).toBe(0); + + // Click toggle button and check if it switches to 'show_less' + fireEvent.click(toggleButton); + expect(screen.getByText('dashboard.achievements.show_less')).toBeDefined(); + }); +}); diff --git a/scratch/ProfileComparisonAnalytics.tsx b/scratch/ProfileComparisonAnalytics.tsx new file mode 100644 index 000000000..897691342 --- /dev/null +++ b/scratch/ProfileComparisonAnalytics.tsx @@ -0,0 +1,970 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { + Award, + Trophy, + Flame, + Sparkles, + Star, + GitFork, + GitCommit, + MapPin, + Calendar, + Check, + Lock, + ExternalLink, + ChevronDown, + ChevronUp +} from 'lucide-react'; +import { useTranslation } from '@/context/TranslationContext'; +import type { + UserProfile, + UserStats, + LanguageData, + ActivityData, + Achievement, + Repository +} from '@/types/dashboard'; + +// Types for the comparison component input +export interface CompareUser { + profile: UserProfile; + stats: UserStats; + languages: LanguageData[]; + activity: ActivityData[]; + achievements: Achievement[]; + popularRepos?: Repository[]; +} + +interface ProfileComparisonAnalyticsProps { + user1: CompareUser; + user2: CompareUser; +} + +export default function ProfileComparisonAnalytics({ + user1, + user2 +}: ProfileComparisonAnalyticsProps) { + const { t } = useTranslation(); + + // Safely fallback for User 1 + const u1Profile = useMemo(() => user1?.profile || { + username: 'user1', + name: 'User One', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, [user1]); + + const u1Stats = useMemo(() => user1?.stats || { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, [user1]); + const u1Languages = useMemo(() => user1?.languages || [], [user1]); + const u1Activity = useMemo(() => user1?.activity || [], [user1]); + const u1Achievements = useMemo(() => user1?.achievements || [], [user1]); + const u1Repos = useMemo(() => user1?.popularRepos || [], [user1]); + + // Safely fallback for User 2 + const u2Profile = useMemo(() => user2?.profile || { + username: 'user2', + name: 'User Two', + avatarUrl: '', + isPro: false, + bio: '', + location: '', + joinedDate: '', + developerScore: 0, + stats: { repositories: 0, followers: 0, following: 0, stars: 0 } + }, [user2]); + + const u2Stats = useMemo(() => user2?.stats || { currentStreak: 0, peakStreak: 0, totalContributions: 0 }, [user2]); + const u2Languages = useMemo(() => user2?.languages || [], [user2]); + const u2Activity = useMemo(() => user2?.activity || [], [user2]); + const u2Achievements = useMemo(() => user2?.achievements || [], [user2]); + const u2Repos = useMemo(() => user2?.popularRepos || [], [user2]); + + // State for toggling achievement lists + const [showAllAchievements, setShowAllAchievements] = useState(false); + + // 1. Process Activity month-by-month + const monthlyData = useMemo(() => { + const monthlyMap: Record = {}; + + const processAct = (act: ActivityData[], key: 'user1' | 'user2') => { + if (!Array.isArray(act)) return; + act.forEach((item) => { + if (!item.date) return; + const parts = item.date.split('-'); + if (parts.length < 2) return; + const year = parts[0]; + const month = parts[1]; + const monthYear = `${year}-${month}`; + if (!monthlyMap[monthYear]) { + monthlyMap[monthYear] = { user1: 0, user2: 0 }; + } + monthlyMap[monthYear][key] += item.count || 0; + }); + }; + + processAct(u1Activity, 'user1'); + processAct(u2Activity, 'user2'); + + const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + + return Object.entries(monthlyMap) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([monthYear, counts]) => { + const [year, monthStr] = monthYear.split('-'); + const monthIdx = parseInt(monthStr, 10) - 1; + const monthKey = monthNames[monthIdx] || monthStr; + const localizedMonthName = t(`dashboard.compare.months.${monthKey}`, { defaultValue: monthKey.toUpperCase() }); + return { + label: `${localizedMonthName} ${year}`, + ...counts + }; + }); + }, [u1Activity, u2Activity, t]); + + const maxMonthlyVal = useMemo(() => { + if (monthlyData.length === 0) return 1; + const maxVal = Math.max(...monthlyData.map((d) => Math.max(d.user1, d.user2)), 0); + return maxVal === 0 ? 1 : maxVal; + }, [monthlyData]); + + // 2. Process Languages side-by-side + const combinedLanguages = useMemo(() => { + const allLangs: Record = {}; + + u1Languages.forEach((l) => { + if (!l.name) return; + allLangs[l.name] = { + name: l.name, + color: l.color || '#94a3b8', + user1Pct: typeof l.percentage === 'number' && !isNaN(l.percentage) ? l.percentage : 0, + user2Pct: 0 + }; + }); + + u2Languages.forEach((l) => { + if (!l.name) return; + if (allLangs[l.name]) { + allLangs[l.name].user2Pct = typeof l.percentage === 'number' && !isNaN(l.percentage) ? l.percentage : 0; + } else { + allLangs[l.name] = { + name: l.name, + color: l.color || '#94a3b8', + user1Pct: 0, + user2Pct: typeof l.percentage === 'number' && !isNaN(l.percentage) ? l.percentage : 0 + }; + } + }); + + return Object.values(allLangs) + .sort((a, b) => (b.user1Pct + b.user2Pct) - (a.user1Pct + a.user2Pct)); + }, [u1Languages, u2Languages]); + + // 3. Process Achievements comparison + const mergedAchievements = useMemo(() => { + const map: Record< + string, + { + id: string; + title: string; + description: string; + type: Achievement['type']; + user1: Achievement | null; + user2: Achievement | null; + } + > = {}; + + u1Achievements.forEach((a) => { + if (!a.id) return; + map[a.id] = { + id: a.id, + title: a.title || 'Achievement', + description: a.description || '', + type: a.type || 'contributions', + user1: a, + user2: null + }; + }); + + u2Achievements.forEach((a) => { + if (!a.id) return; + if (map[a.id]) { + map[a.id].user2 = a; + } else { + map[a.id] = { + id: a.id, + title: a.title || 'Achievement', + description: a.description || '', + type: a.type || 'contributions', + user1: null, + user2: a + }; + } + }); + + return Object.values(map); + }, [u1Achievements, u2Achievements]); + + const visibleAchievements = showAllAchievements + ? mergedAchievements + : mergedAchievements.slice(0, 4); + + // 4. Process and Rank Top 3 Repositories + const processTopRepos = (repos: Repository[]) => { + if (!Array.isArray(repos)) return []; + return repos + .map((repo) => { + // Resolve keys + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const commits = (repo as any).commits ?? (repo as any).commitCount ?? 0; + const stars = repo.stargazerCount ?? (repo as any).stars ?? 0; + const forks = repo.forkCount ?? (repo as any).forks ?? 0; + + // Impact Score: (commits * 3) + (stars * 5) + (forks * 10) + const score = commits * 3 + stars * 5 + forks * 10; + + let langName = 'Unknown'; + let langColor = '#94a3b8'; + if (repo.primaryLanguage) { + langName = repo.primaryLanguage.name || 'Unknown'; + langColor = repo.primaryLanguage.color || '#94a3b8'; + } + + return { + name: repo.name || 'unnamed-repo', + description: repo.description || '', + commits, + stars, + forks, + score, + url: repo.url || '#', + language: { name: langName, color: langColor } + }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, 3); + }; + + const u1TopRepos = useMemo(() => processTopRepos(u1Repos), [u1Repos]); + const u2TopRepos = useMemo(() => processTopRepos(u2Repos), [u2Repos]); + + // Symmetrical layout requires rendering indices 0, 1, 2 head-to-head + const maxRepoRank = 3; + + return ( +
+ {/* Header section with profile cards & VS floating circle */} +
+ + {/* Floating VS middle divider */} + + + {/* User 1 profile card */} + + {u1Profile.isPro && ( +
+ {t('dashboard.profile.pro')} +
+ )} + +
+
+
+ {u1Profile.avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {u1Profile.name + ) : ( +
+ {u1Profile.username.substring(0, 2).toUpperCase()} +
+ )} +
+
+

+ {u1Profile.name || u1Profile.username} +

+

@{u1Profile.username}

+ +
+ + + {t('dashboard.compare.developer_score')}: {u1Profile.developerScore} + +
+
+
+ + {u1Profile.bio && ( +

+ {u1Profile.bio} +

+ )} + +
+ {u1Profile.location && ( + + {u1Profile.location} + + )} + {u1Profile.joinedDate && ( + + {u1Profile.joinedDate} + + )} +
+
+ +
+
+ + {u1Profile.stats?.repositories || 0} + + {t('dashboard.profile.repos')} +
+
+ + {u1Profile.stats?.followers || 0} + + {t('dashboard.profile.followers')} +
+
+ + {u1Stats.totalContributions || 0} + + {t('dashboard.stats.contributions')} +
+
+
+ + {/* User 2 profile card */} + + {u2Profile.isPro && ( +
+ {t('dashboard.profile.pro')} +
+ )} + +
+
+
+ {u2Profile.avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {u2Profile.name + ) : ( +
+ {u2Profile.username.substring(0, 2).toUpperCase()} +
+ )} +
+
+

+ {u2Profile.name || u2Profile.username} +

+

@{u2Profile.username}

+ +
+ + + {t('dashboard.compare.developer_score')}: {u2Profile.developerScore} + +
+
+
+ + {u2Profile.bio && ( +

+ {u2Profile.bio} +

+ )} + +
+ {u2Profile.location && ( + + {u2Profile.location} + + )} + {u2Profile.joinedDate && ( + + {u2Profile.joinedDate} + + )} +
+
+ +
+
+ + {u2Profile.stats?.repositories || 0} + + {t('dashboard.profile.repos')} +
+
+ + {u2Profile.stats?.followers || 0} + + {t('dashboard.profile.followers')} +
+
+ + {u2Stats.totalContributions || 0} + + {t('dashboard.stats.contributions')} +
+
+
+
+ + {/* Contribution Comparison Section */} + +
+

+ {t('dashboard.compare.contribution_comparison.title')} +

+

+ {t('dashboard.compare.contribution_comparison.subtitle')} +

+
+ + {monthlyData.length === 0 ? ( +
+ {t('dashboard.compare.contribution_comparison.no_data')} +
+ ) : ( +
+ {/* Visual Bar Chart */} +
+ {monthlyData.map((data, idx) => { + const u1Pct = (data.user1 / maxMonthlyVal) * 100; + const u2Pct = (data.user2 / maxMonthlyVal) * 100; + + return ( +
+ + {/* Hover Stats Tooltip */} +
+

{data.label}

+

@{u1Profile.username}: {data.user1}

+

@{u2Profile.username}: {data.user2}

+
+ + {/* The double bar */} +
+ {/* User 1 bar (emerald) */} +
+
+
+ + {/* User 2 bar (indigo) */} +
+
+
+
+ + {/* Month label */} + + {data.label.split(' ')[0]} + +
+ ); + })} +
+ + {/* Quick legend with totals */} +
+
+ + + {t('dashboard.compare.contribution_comparison.user_total', { + name: `@${u1Profile.username}`, + count: u1Stats.totalContributions.toLocaleString() + })} + + + + {t('dashboard.compare.contribution_comparison.user_total', { + name: `@${u2Profile.username}`, + count: u2Stats.totalContributions.toLocaleString() + })} + +
+
+
+ )} + + + {/* Language Breakdown Section */} + +
+

+ {t('dashboard.compare.language_comparison.title')} +

+

+ {t('dashboard.compare.language_comparison.subtitle')} +

+
+ + {combinedLanguages.length === 0 ? ( +
+ {t('dashboard.compare.language_comparison.no_data')} +
+ ) : ( +
+ {combinedLanguages.map((lang) => ( +
+ {/* User 1 language percentage */} +
+ + {lang.user1Pct.toFixed(1)}% + +
+
+
+
+ + {/* Center language badge */} +
+ + + {lang.name} + +
+ + {/* User 2 language percentage */} +
+
+
+
+ + {lang.user2Pct.toFixed(1)}% + +
+
+ ))} +
+ )} + + + {/* Achievement Comparison Section */} + +
+
+

+ {t('dashboard.compare.achievement_comparison.title')} +

+

+ {t('dashboard.compare.achievement_comparison.subtitle')} +

+
+
+ + {mergedAchievements.length === 0 ? ( +
+ {t('dashboard.compare.achievement_comparison.no_data')} +
+ ) : ( +
+
+ @{u1Profile.username} + {t('dashboard.achievements.title')} + @{u2Profile.username} +
+ +
+ {visibleAchievements.map((item) => { + const isUnlocked1 = item.user1?.isUnlocked || false; + const isUnlocked2 = item.user2?.isUnlocked || false; + + // Let's decide how to display who unlocked it + let unlockedBy = t('dashboard.compare.achievement_comparison.neither'); + if (isUnlocked1 && isUnlocked2) { + unlockedBy = t('dashboard.compare.achievement_comparison.both'); + } else if (isUnlocked1) { + unlockedBy = `@${u1Profile.username}`; + } else if (isUnlocked2) { + unlockedBy = `@${u2Profile.username}`; + } + + return ( +
+ {/* User 1 Badge */} +
+ {isUnlocked1 ? ( +
+ + {t('dashboard.compare.achievement_comparison.unlocked')} +
+ ) : ( +
+ + {t('dashboard.compare.achievement_comparison.locked')} +
+ )} +
+ + {/* Achievement Details */} +
+
+ {item.type === 'streak' ? ( + + ) : item.type === 'behavior' ? ( + + ) : ( + + )} +

+ {item.title} +

+
+

+ {item.description} +

+ + {t('dashboard.compare.achievement_comparison.unlocked_by')}: {unlockedBy} + +
+ + {/* User 2 Badge */} +
+ {isUnlocked2 ? ( +
+ + {t('dashboard.compare.achievement_comparison.unlocked')} +
+ ) : ( +
+ + {t('dashboard.compare.achievement_comparison.locked')} +
+ )} +
+
+ ); + })} +
+ + {mergedAchievements.length > 4 && ( + + )} +
+ )} +
+ + {/* Repository Comparison Section */} + +
+

+ {t('dashboard.compare.repository_comparison.title')} +

+

+ {t('dashboard.compare.repository_comparison.subtitle')} +

+
+ + {u1TopRepos.length === 0 && u2TopRepos.length === 0 ? ( +
+ {t('dashboard.compare.repository_comparison.no_data')} +
+ ) : ( +
+ {Array.from({ length: maxRepoRank }).map((_, rankIdx) => { + const repo1 = u1TopRepos[rankIdx]; + const repo2 = u2TopRepos[rankIdx]; + + // If neither has a repository at this rank, don't render it + if (!repo1 && !repo2) return null; + + // Highlighting winners for individual stats + const scoreWinner = repo1 && repo2 ? (repo1.score > repo2.score ? 'user1' : repo1.score < repo2.score ? 'user2' : 'tie') : null; + const commitsWinner = repo1 && repo2 ? (repo1.commits > repo2.commits ? 'user1' : repo1.commits < repo2.commits ? 'user2' : 'tie') : null; + const starsWinner = repo1 && repo2 ? (repo1.stars > repo2.stars ? 'user1' : repo1.stars < repo2.stars ? 'user2' : 'tie') : null; + const forksWinner = repo1 && repo2 ? (repo1.forks > repo2.forks ? 'user1' : repo1.forks < repo2.forks ? 'user2' : 'tie') : null; + + return ( +
+ {/* Repo 1 details (Left) */} +
+ {repo1 ? ( +
+
+

+ {repo1.name} +

+ {repo1.language.name !== 'Unknown' && ( + + + {repo1.language.name} + + )} + + + +
+ {repo1.description && ( +

+ {repo1.description} +

+ )} + +
+
+ {t('dashboard.compare.repository_comparison.commits')} + + {repo1.commits} + +
+
+ {t('dashboard.compare.repository_comparison.stars')} + + {repo1.stars} + +
+
+ {t('dashboard.compare.repository_comparison.forks')} + + {repo1.forks} + +
+
+ {t('dashboard.compare.repository_comparison.impact_score')} + + {repo1.score} + +
+
+
+ ) : ( +
+ N/A +
+ )} +
+ + {/* Central Rank Indicator */} +
+ + {t('dashboard.compare.repository_comparison.rank')} + + + #{rankIdx + 1} + +
+ + {/* Repo 2 details (Right) */} +
+ {repo2 ? ( +
+
+ + + + {repo2.language.name !== 'Unknown' && ( + + + {repo2.language.name} + + )} +

+ {repo2.name} +

+
+ {repo2.description && ( +

+ {repo2.description} +

+ )} + +
+
+ {t('dashboard.compare.repository_comparison.commits')} + + {repo2.commits} + +
+
+ {t('dashboard.compare.repository_comparison.stars')} + + {repo2.stars} + +
+
+ {t('dashboard.compare.repository_comparison.forks')} + + {repo2.forks} + +
+
+ {t('dashboard.compare.repository_comparison.impact_score')} + + {repo2.score} + +
+
+
+ ) : ( +
+ N/A +
+ )} +
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/scratch/RepositoryImpactAnalyzer.test.tsx b/scratch/RepositoryImpactAnalyzer.test.tsx new file mode 100644 index 000000000..4c1459db5 --- /dev/null +++ b/scratch/RepositoryImpactAnalyzer.test.tsx @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import RepositoryImpactAnalyzer, { formatAge } from './RepositoryImpactAnalyzer'; + +// Mock framer-motion to render clean HTML containers for testing +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, className, style, ...props }: any) => { + delete props.initial; + delete props.animate; + delete props.whileInView; + delete props.viewport; + delete props.transition; + return ( +
+ {children} +
+ ); + }, + }, +})); + +// Helper to create dates relative to today +function getRelativeDateString(monthsAgo: number): string { + const d = new Date(); + d.setMonth(d.getMonth() - monthsAgo); + return d.toISOString(); +} + +describe('RepositoryImpactAnalyzer', () => { + it('renders an empty state when no repositories are provided', () => { + render(); + + expect(screen.getByText('Repository Impact Analyzer')).toBeDefined(); + expect(screen.getByText('No repository data available.')).toBeDefined(); + }); + + it('correctly ranks repositories and limits to top 5 based on weighted impact score', () => { + // Score formulas: (commits * 3) + (stars * 5) + (forks * 10) + const repos = [ + { name: 'Repo1', commits: 10, stars: 10, forks: 10, createdAt: getRelativeDateString(10) }, // 30 + 50 + 100 = 180 + { name: 'Repo2', commits: 50, stars: 20, forks: 5, createdAt: getRelativeDateString(10) }, // 150 + 100 + 50 = 300 + { name: 'Repo3', commits: 5, stars: 2, forks: 0, createdAt: getRelativeDateString(10) }, // 15 + 10 + 0 = 25 + { name: 'Repo4', commits: 100, stars: 100, forks: 50, createdAt: getRelativeDateString(10) },// 300 + 500 + 500 = 1300 + { name: 'Repo5', commits: 20, stars: 50, forks: 12, createdAt: getRelativeDateString(10) }, // 60 + 250 + 120 = 430 + { name: 'Repo6', commits: 80, stars: 40, forks: 8, createdAt: getRelativeDateString(10) }, // 240 + 200 + 80 = 520 + ]; + + render(); + + // Top 5 rank order expected: Repo4 (1300), Repo6 (520), Repo5 (430), Repo2 (300), Repo1 (180) + // Repo3 (25) should be excluded (ranked 6th) + + expect(screen.getByText('Repo4')).toBeDefined(); + expect(screen.getByText('Repo6')).toBeDefined(); + expect(screen.getByText('Repo5')).toBeDefined(); + expect(screen.getByText('Repo2')).toBeDefined(); + expect(screen.getByText('Repo1')).toBeDefined(); + expect(screen.queryByText('Repo3')).toBeNull(); + + // Verify correct score calculations are rendered + expect(screen.getByText('1300')).toBeDefined(); + expect(screen.getByText('520')).toBeDefined(); + expect(screen.getByText('430')).toBeDefined(); + expect(screen.getByText('300')).toBeDefined(); + expect(screen.getByText('180')).toBeDefined(); + }); + + it('accurately calculates and renders growth metrics', () => { + // Create a repo exactly 10 months old + const tenMonthsAgo = getRelativeDateString(10); + const repos = [ + { + name: 'Repo-Growth', + commits: 50, + stars: 30, + forks: 20, + createdAt: tenMonthsAgo, + }, + ]; + + render(); + + // Individual repo metrics verification: + // Age = 10 months + expect(screen.getAllByText('10 months').length).toBeGreaterThanOrEqual(1); + + // Overall growth metrics cards verification: + // Avg Stars/Mo overall = 30 / 10 = 3 + // Avg Forks/Mo overall = 20 / 10 = 2 + expect(screen.getAllByText('3').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('2').length).toBeGreaterThanOrEqual(1); + }); + + it('computes language contributions percentage correctly', () => { + const repos = [ + { + name: 'TS-Repo', + commits: 70, + primaryLanguage: { name: 'TypeScript', color: '#3178c6' }, + }, + { + name: 'JS-Repo', + commits: 30, + primaryLanguage: { name: 'JavaScript', color: '#f1e05a' }, + }, + ]; + + render(); + + // Total commits = 100. TypeScript = 70%, JavaScript = 30% + expect(screen.getAllByText('TypeScript').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('JavaScript').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('70%').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('30%').length).toBeGreaterThanOrEqual(1); + }); + + it('complies with accessibility (a11y) standards', () => { + const repos = [ + { + name: 'A11y-Repo', + commits: 10, + stars: 5, + forks: 2, + createdAt: getRelativeDateString(5), + }, + ]; + + const { container } = render(); + + // Region role and labelling check + const region = screen.getByRole('region'); + expect(region).toBeDefined(); + expect(region.getAttribute('aria-labelledby')).toBe('impact-analyzer-title'); + + // Heading has matching id + const heading = screen.getByRole('heading', { level: 3 }); + expect(heading.id).toBe('impact-analyzer-title'); + + // Language bar has image role with description + const imgElement = screen.getByRole('img'); + expect(imgElement).toBeDefined(); + expect(imgElement.getAttribute('aria-label')).toContain('Language contribution'); + + // Ranking link has informative aria-label + const link = screen.getByRole('link'); + expect(link.getAttribute('aria-label')).toContain('Rank 1'); + }); + + it('formats age correctly via helper function', () => { + const mockT = (key: string) => { + if (key === 'dashboard.impact.months') return 'months'; + if (key === 'dashboard.impact.years') return 'years'; + return key; + }; + + expect(formatAge(5, mockT)).toBe('5 months'); + expect(formatAge(12, mockT)).toBe('1 year'); + expect(formatAge(24, mockT)).toBe('2 years'); + expect(formatAge(15, mockT)).toBe('1y 3m'); + }); +}); diff --git a/scratch/RepositoryImpactAnalyzer.tsx b/scratch/RepositoryImpactAnalyzer.tsx new file mode 100644 index 000000000..f9d58a083 --- /dev/null +++ b/scratch/RepositoryImpactAnalyzer.tsx @@ -0,0 +1,347 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { Award, GitFork, Star, TrendingUp, Calendar, BookOpen } from 'lucide-react'; +import { useTranslation } from '@/context/TranslationContext'; + +export interface RepositoryImpactAnalyzerProps { + repositories: Array<{ + name: string; + commits?: number; + commitCount?: number; + stars?: number; + stargazerCount?: number; + forks?: number; + forkCount?: number; + createdAt?: string | Date; + created_at?: string | Date; + language?: { + name: string; + color: string; + } | string | null; + primaryLanguage?: { + name: string; + color: string; + } | string | null; + url?: string; + }>; +} + +export function formatAge(months: number, t: (key: string) => string): string { + if (months < 12) { + return `${months} ${t('dashboard.impact.months')}`; + } + const years = Math.floor(months / 12); + const remainingMonths = months % 12; + if (remainingMonths === 0) { + const unit = years === 1 ? t('dashboard.impact.years').replace(/s$/, '') : t('dashboard.impact.years'); + return `${years} ${unit}`; + } + return `${years}y ${remainingMonths}m`; +} + +export default function RepositoryImpactAnalyzer({ repositories = [] }: RepositoryImpactAnalyzerProps) { + const { t } = useTranslation(); + + // Handle empty state gracefully + if (!repositories || repositories.length === 0) { + return ( + +

+ + {t('dashboard.impact.title')} +

+

+ {t('dashboard.impact.no_data')} +

+
+ ); + } + + // Sanitize and process repositories + const processedRepos = repositories.map((repo) => { + const commits = repo.commits ?? repo.commitCount ?? 0; + const stars = repo.stars ?? repo.stargazerCount ?? 0; + const forks = repo.forks ?? repo.forkCount ?? 0; + + // Resolve Language name and color + let langName = 'Unknown'; + let langColor = '#94a3b8'; // default slate-400 + + const langObj = repo.primaryLanguage ?? repo.language; + if (langObj) { + if (typeof langObj === 'object') { + langName = langObj.name ?? 'Unknown'; + langColor = langObj.color ?? '#94a3b8'; + } else if (typeof langObj === 'string') { + langName = langObj; + } + } + + // Resolve repository age in months + const now = new Date(); + const createdRaw = repo.createdAt ?? repo.created_at ?? now; + const created = new Date(createdRaw); + const validCreated = isNaN(created.getTime()) ? now : created; + const diffYears = now.getFullYear() - validCreated.getFullYear(); + const diffMonths = now.getMonth() - validCreated.getMonth(); + // age must be at least 1 month to avoid division by zero + const ageInMonths = Math.max(1, diffYears * 12 + diffMonths); + + // Calculate impact score: (commits * 3) + (stars * 5) + (forks * 10) + const score = commits * 3 + stars * 5 + forks * 10; + + const avgStarsPerMonth = parseFloat((stars / ageInMonths).toFixed(2)); + const avgForksPerMonth = parseFloat((forks / ageInMonths).toFixed(2)); + + return { + name: repo.name, + commits, + stars, + forks, + score, + ageInMonths, + avgStarsPerMonth, + avgForksPerMonth, + language: { + name: langName, + color: langColor, + }, + url: repo.url ?? '#', + }; + }); + + // Rank Top 5 Repositories based on Score + const topRepos = [...processedRepos].sort((a, b) => b.score - a.score).slice(0, 5); + + // Group commits by repository language + const languageGroups: Record = {}; + let totalCommits = 0; + + processedRepos.forEach((repo) => { + const lang = repo.language.name; + const color = repo.language.color; + const commits = repo.commits; + + totalCommits += commits; + if (!languageGroups[lang]) { + languageGroups[lang] = { commits: 0, color }; + } + languageGroups[lang].commits += commits; + }); + + const languageContribution = Object.entries(languageGroups) + .map(([name, data]) => { + const percentage = totalCommits > 0 ? parseFloat(((data.commits / totalCommits) * 100).toFixed(1)) : 0; + return { + name, + commits: data.commits, + color: data.color, + percentage, + }; + }) + .sort((a, b) => b.commits - a.commits); + + // Calculate Overall Growth Metrics + const totalAgeMonths = processedRepos.reduce((acc, repo) => acc + repo.ageInMonths, 0); + const avgAgeMonths = processedRepos.length > 0 ? totalAgeMonths / processedRepos.length : 0; + const totalStars = processedRepos.reduce((acc, repo) => acc + repo.stars, 0); + const totalForks = processedRepos.reduce((acc, repo) => acc + repo.forks, 0); + + const avgStarsPerMonthOverall = totalAgeMonths > 0 ? parseFloat((totalStars / totalAgeMonths).toFixed(2)) : 0; + const avgForksPerMonthOverall = totalAgeMonths > 0 ? parseFloat((totalForks / totalAgeMonths).toFixed(2)) : 0; + + return ( + + {/* Title Header */} +
+
+
+ +
+
+

+ {t('dashboard.impact.title')} +

+

+ {t('dashboard.impact.growth')} & ranking analysis +

+
+
+
+ + {/* Main Grid Layout */} +
+ + {/* Left Column: Top 5 Repositories ranking */} + + + {/* Right Column: Language Contribution & Overall Growth Metrics */} +
+ + {/* Language Contribution */} +
+

+ {t('dashboard.impact.languages')} +

+ + {/* Responsive Segment Bar */} +
+ {languageContribution.map((lang) => ( + + ))} +
+ + {/* Language Legend */} +
+ {languageContribution.slice(0, 4).map((lang) => ( +
+
+ + {lang.name} +
+ + {lang.percentage}% + +
+ ))} +
+
+ + {/* Growth Metrics Summary Cards */} +
+

+ + {t('dashboard.impact.growth')} +

+ +
+ {/* Avg Age */} +
+
+ + Age +
+
+ {formatAge(Math.round(avgAgeMonths), t)} +
+
+ + {/* Avg Stars / Month */} +
+
+ + Stars/Mo +
+
+ {avgStarsPerMonthOverall} +
+
+ + {/* Avg Forks / Month */} +
+
+ + Forks/Mo +
+
+ {avgForksPerMonthOverall} +
+
+
+
+ +
+ +
+
+ ); +} diff --git a/scratch/de.json b/scratch/de.json new file mode 100644 index 000000000..1a395b371 --- /dev/null +++ b/scratch/de.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "Zur Startseite", + "repo": "GitHub-Repo", + "contributors": "Mitwirkende", + "theme_toggle": "Theme umschalten", + "menu_open": "Menü öffnen", + "menu_close": "Menü schließen", + "compare": "Vergleichen", + "customization_studio": "Anpassungsstudio", + "generator": "Generator", + "burnout_radar": "Burnout-Radar" + }, + "footer": { + "tagline": "Entwickelt für die Elite-Builder-Community.", + "contributors": "Mitwirkende", + "documentation": "Dokumentation", + "creator": "Ersteller", + "copyright": "© {{year}} CommitPulse. Alle Rechte vorbehalten.", + "navigation": "Navigation", + "resources": "Ressourcen", + "connect": "Verbinden", + "made_with": "Mit ❤️ für Entwickler erstellt", + "home": "Startseite", + "compare": "Vergleichen", + "customization": "Anpassung", + "github_repo": "GitHub-Repository", + "github": "GitHub", + "creator_github": "Ersteller auf GitHub", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "Generator" + }, + "landing": { + "empty_username_warning": "Bitte gib einen GitHub-Benutzernamen ein, um deinen Badge-Link zu kopieren.", + "max_length_warning": "GitHub-Benutzername-Limit erreicht (maximal 39 Zeichen)", + "title": "Werte deine\n{Contribution}-Story auf.", + "subtitle": "Gib dich nicht mehr mit flachen Rastern zufrieden. Erzeuge hochauflösende, isometrische 3D-Monolithen, die deinen Coding-Rhythmus mit professioneller Präzision visualisieren.", + "watch_dashboard": "Dashboard ansehen", + "discord_community": "Tritt der Core-Community auf Discord bei", + "input_placeholder": "GitHub-Benutzernamen eingeben", + "clear_input": "Eingabe löschen", + "copy_link": "Link kopieren", + "copied": "Kopiert", + "recent": "Kürzlich:", + "clear": "Löschen", + "preview_placeholder_title": "Bereit, deinen Rhythmus zu visualisieren?", + "preview_placeholder_desc": "Gib oben einen GitHub-Benutzernamen ein, um sofort dein Streak-Badge zu erzeugen.", + "user_not_found": "GitHub-Benutzer nicht gefunden", + "user_not_found_desc": "Bitte überprüfe den Benutzernamen und versuche es erneut.", + "preview_auto_theme": "Zufälliges Theme ändert sich bei jedem Seitenaufruf und deaktiviert das Caching", + "preview_caching_tip": "Vorschau aktualisiert sich bei jeder Änderung. Das gehostete Badge wird um UTC-Mitternacht zwischengespeichert", + "preview_empty_tip": "Füge einen Benutzernamen hinzu, um Live-Vorschau und Export-Snippets zu aktivieren", + "features": { + "sync_title": "Echtzeit-Sync", + "sync_desc": "Direkt von der GitHub-GraphQL-API abgerufen. Dein Streak aktualisiert sich so schnell, wie du Code pushst.", + "theme_title": "Theme-Engine", + "theme_desc": "Wechsle zwischen Neon, Dracula oder benutzerdefinierten HEX-Modi über einfache URL-Verwaltung.", + "isometric_title": "Isometrische Mathematik", + "isometric_desc": "Ausgefeilte 3D-Projektionsformeln verwandeln 2D-Daten in digitale Architektur." + }, + "generate_badge": "Badge erzeugen", + "verified_profile": "Verifiziertes Profil", + "verifying": "Wird verifiziert ...", + "preview_monolith": "MONOLITH-VORSCHAU", + "interactive_preview_title": "Interaktive Monolith-Vorschau", + "interactive_preview_desc": "CommitPulse fasst deinen öffentlichen GitHub-Beitragsverlauf zu einer anpassbaren 3D-Stadt zusammen. Je höher die Türme, desto mehr hast du an diesem Tag committet. Gib oben einen GitHub-Benutzernamen ein, um sofort dein Streak-Badge zu erzeugen.", + "input_aria_label": "GitHub-Benutzernamen eingeben, um ein Badge zu erzeugen", + "unable_to_load_stats": "Statistiken konnten nicht geladen werden" + }, + "success_guide": { + "title": "Dein Monolith ist bereit – setze ihn in 4 Schritten ein", + "markdown_copied": "Markdown kopiert", + "dismiss_aria": "Anleitung schließen", + "step_1_title": "Öffne dein Profil-Repo", + "step_1_body": "Gehe zu github.com/DEIN_BENUTZERNAME/DEIN_BENUTZERNAME – deinem speziellen Profil-Repository.", + "step_2_title": "README.md bearbeiten", + "step_2_body": "Klicke auf das Stift-Symbol, um die Datei im integrierten Editor von GitHub zu öffnen.", + "step_3_title": "Snippet einfügen", + "step_3_body": "Setze den Cursor an die Stelle, an der der Monolith erscheinen soll, und füge ihn dann ein (Strg+V / Cmd+V).", + "step_4_title": "Speichern & veröffentlichen", + "step_4_body": "Klicke auf „Commit changes“ und besuche dein Profil. Dein 3D-Streak ist jetzt live.", + "copied_snippet_label": "Dein kopiertes Snippet", + "color_tip": "Tipp: Füge der URL ?accent=808080 hinzu, um die Farbpalette deines Monolithen zu ändern.", + "watch_dashboard_btn": "Dein Dashboard ansehen", + "customize_more_btn": "Möchtest du mehr anpassen?" + }, + "customize_cta": { + "studio_badge": "Anpassungsstudio", + "title": "Möchtest du deinen Monolithen feinabstimmen?", + "desc": "Stelle jedes Pixel genau ein – tausche Akzentfarben, probiere ein dunkles oder Neon-Theme, schalte die logarithmische Höhenskalierung um und sieh dir Änderungen live an, bevor du eine einzige Zeile einfügst.", + "btn": "Anpassungsstudio öffnen" + }, + "customize": { + "back_to_home": "Zurück zur Startseite", + "title": "Stimme deinen Monolithen fein ab.", + "desc": "Jede Änderung unten aktualisiert die Vorschau in Echtzeit. Kopiere das Export-Snippet, wenn du fertig bist. Keine weiteren Schritte nötig.", + "live_preview": "Live-Vorschau", + "active_params": "Aktive Parameter", + "empty_preview_desc": "Die Live-Badge-Vorschau erscheint hier, sobald ein Benutzername hinzugefügt wurde.", + "controls": { + "username": "GitHub-Benutzername", + "username_placeholder": "Benutzernamen eingeben ...", + "select_theme": "Theme auswählen", + "theme_placeholder": "Voreingestelltes Theme auswählen", + "theme_mode": "Theme-Modus", + "custom_bg": "Benutzerdefinierter Hintergrund", + "custom_accent": "Benutzerdefinierter Akzent", + "custom_text": "Benutzerdefinierter Text", + "log_scaling": "Logarithmische Höhenskalierung", + "speed": "Animationsgeschwindigkeit", + "radius": "Eckenradius", + "badge_size": "Badge-Größe", + "sync_year": "Sync-Jahr", + "clear_custom": "Benutzerdefinierte Farben zurücksetzen", + "theme_presets": "Theme-Voreinstellung", + "color_overrides": "Benutzerdefinierte Farbüberschreibungen", + "font": "Schriftart", + "custom_font_option": "Benutzerdefinierte Google-Schriftart ...", + "custom_font_placeholder": "z. B. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "Export-Snippet", + "snippet_desc": "Wechsle Formate, ohne die Live-Badge-Konfiguration zu ändern.", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Badge herunterladen", + "downloading": "Wird heruntergeladen ...", + "download_not_available": "Download nicht verfügbar", + "copy_workflow": "Workflow kopieren", + "copy_format": "{{format}} kopieren", + "copied": "Kopiert!", + "copy_aria_disabled": "Füge einen GitHub-Benutzernamen hinzu, um das Kopieren des {{format}}-Export-Snippets zu aktivieren", + "copy_aria_enabled": "{{format}}-Export-Snippet in die Zwischenablage kopieren", + "footer_tip": "Füge dies in die README.md deines GitHub-Profils ein. Das Badge wird serverseitig gerendert, kein Skript erforderlich.", + "tsx": "React TSX", + "download_svg": "SVG herunterladen", + "download_png": "PNG herunterladen" + } + }, + "dashboard": { + "generate_btn": "Erstelle dein eigenes Dashboard", + "refresh_btn": "Daten aktualisieren", + "refreshing": "Wird aktualisiert ...", + "refreshed_toast": "Dashboard erfolgreich aktualisiert", + "profile": { + "score": "Entwickler-Score", + "repos": "Repositories", + "followers": "Follower", + "following": "Folgt", + "stars": "Sterne", + "joined": "Beigetreten", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Teile deinen Pulse" + }, + "achievements": { + "title": "Erfolge", + "see_all": "Alle Erfolge ansehen", + "show_less": "Weniger anzeigen" + }, + "activity": { + "title": "Aktivitätslandschaft", + "year": "Jahr", + "month": "Monat", + "week": "Woche", + "commits": "Commits", + "intensity": "Commit-Intensität", + "loc": "Codezeilen", + "loc_desc": "Im Zeitverlauf geänderte Codezeilen", + "commits_desc": "Commit-Häufigkeit im Zeitverlauf", + "lines_modified": "{{count}} Zeilen geändert", + "aria_range": "von {{start}} bis {{end}}", + "aria_single": "am {{date}}" + }, + "languages": { + "title": "Top-Sprachen", + "primary": "Hauptsprache", + "no_data": "Keine Sprachdaten gefunden" + }, + "clock": { + "title": "Commit-Uhr", + "active_days": "Wöchentlicher Aktivitätszyklus", + "tooltip_no_commits": "Keine Commits für diesen Wochentag erfasst", + "tooltip_peak": "Aktivster Wochentag in diesem Zyklus", + "tooltip_activity": "Wöchentlicher Aktivitätspunkt" + }, + "heatmap": { + "title": "Beitrags-Heatmap", + "less": "Weniger", + "more": "Mehr", + "last_365": "Letzte 365 Tage", + "tooltip_single": "{{count}} Beitrag am {{date}}", + "tooltip_plural": "{{count}} Beiträge am {{date}}", + "no_activity": "Keine Aktivität erfasst", + "peak_activity": "Aktivster Tag", + "high_activity": "Tag mit hoher Aktivität", + "steady_contribution": "Tag mit gleichmäßigen Beiträgen", + "light_activity": "Tag mit geringer Aktivität", + "no_active_streak": "Keine aktive Serie", + "active_streak": "{{streak}}-tägige aktive Serie", + "empty": "Keine aktuelle Aktivität zum Anzeigen", + "code_activity": "Code-Aktivität erfasst", + "no_code_changes": "Keine Code-Änderungen erfasst" + }, + "insights": { + "title": "KI-Einblicke" + }, + "stats": { + "current_streak": "Aktuelle Serie", + "peak_streak": "Höchste Serie", + "contributions": "Beiträge", + "days": "Tage", + "last_year": "Letztes Jahr", + "utc_disclaimer": "Serie in UTC-Zeitzone berechnet" + }, + "share": { + "title": "Pulse teilen", + "close_aria": "Teilen-Panel schließen", + "copy_link": "Link kopieren", + "copy_link_desc": "Profil-URL in die Zwischenablage kopieren", + "link_copied": "Link kopiert", + "share_x": "Auf X teilen", + "share_x_desc": "Teile deinen Pulse mit der Welt", + "share_linkedin": "LinkedIn", + "share_linkedin_desc": "Teile deine Entwickler-Aktivität mit deinem Netzwerk", + "copy_markdown": "README-Markdown kopieren", + "copy_markdown_desc": "Markdown-Snippet für deine README kopieren", + "download_png": "PNG-Snapshot herunterladen", + "download_png_desc": "Speichere einen Snapshot deines Dashboards", + "download_svg": "Vektor-SVG-Monolith herunterladen", + "download_svg_desc": "Lade das rohe Monolith-SVG herunter", + "download_json": "Strukturierte JSON-Daten exportieren", + "download_json_desc": "Exportiere rohe Streak- und Sprachdaten", + "share_os": "System-Teilen", + "share_os_desc": "AirDrop, WhatsApp, Messages & mehr", + "share_os_fallback": "Weitere Optionen", + "share_os_fallback_desc": "Öffne den System-Teilen-Dialog", + "share_reddit": "Reddit", + "share_reddit_desc": "Auf Reddit teilen", + "downloaded": "Asset gespeichert!", + "svg_downloaded": "SVG heruntergeladen!", + "json_downloaded": "JSON heruntergeladen!", + "failed": "Fehlgeschlagen – versuche es erneut", + "social_channels": "Social-Media-Kanäle", + "export_options": "Export-Optionen", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Optimiertes WebP herunterladen", + "download_stl": "Druckbares 3D-STL herunterladen" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Beitragsprognose", + "subtitle": "Prognostizieren Sie zukünftiges Wachstum basierend auf Ihrer Programmiergeschwindigkeit und Ihren Trendsteigungen.", + "weekly_velocity": "Wöchentliche Geschwindigkeit", + "monthly_velocity": "Monatliche Geschwindigkeit", + "projected_month": "Prognostiziertes Monatsende", + "projected_year": "Prognostiziertes Jahresende", + "consistency_rating": "Konsistenzbewertung", + "trend_direction": "Trend", + "commits_per_week": "{{count}} Commits/Woche", + "commits_per_month": "{{count}} Commits/Monat", + "commits": "{{count}} Commits", + "growth_rate": "Wachstumsrate: {{rate}} Commits/Tag²", + "consistency_level": { + "elite": "Elite (Sehr konsistent)", + "consistent": "Hoch (Konsistent)", + "occasional": "Moderat (Gelegentlich)", + "sporadic": "Niedrig (Sporadisch)", + "inactive": "Inaktiv" + }, + "trends": { + "strong_growth": "Starkes Wachstum", + "moderate_growth": "Moderates Wachstum", + "stable": "Stabiler Rhythmus", + "cooling": "Abkühlung", + "decline": "Deutlicher Rückgang" + }, + "no_activity": "Keine vergangenen Aktivitätsdaten verfügbar, um Vorhersagen zu generieren." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/en.json b/scratch/en.json new file mode 100644 index 000000000..6809128d5 --- /dev/null +++ b/scratch/en.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "Go to home", + "repo": "GitHub Repo", + "contributors": "Contributors", + "theme_toggle": "Toggle theme", + "menu_open": "Open menu", + "menu_close": "Close menu", + "compare": "Compare", + "customization_studio": "Customization Studio", + "generator": "Generator", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "Designed for the elite builder community.", + "contributors": "Contributors", + "documentation": "Documentation", + "creator": "Creator", + "copyright": "© {{year}} CommitPulse. All rights reserved.", + "navigation": "Navigation", + "resources": "Resources", + "connect": "Connect", + "made_with": "Made with ❤️ for developers", + "home": "Home", + "compare": "Compare", + "customization": "Customization", + "github_repo": "GitHub Repository", + "github": "GitHub", + "creator_github": "Creator on GitHub", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "Generator" + }, + "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", + "title": "Elevate Your\n{Contribution} Story.", + "subtitle": "Stop settling for flat grids. Generate high-fidelity, 3D isometric monoliths that visualize your coding rhythm with professional precision.", + "watch_dashboard": "Watch Dashboard", + "discord_community": "Join the core community on Discord", + "input_placeholder": "Enter GitHub Username", + "clear_input": "Clear input", + "copy_link": "Copy Link", + "copied": "Copied", + "recent": "Recent:", + "clear": "Clear", + "preview_placeholder_title": "Ready to visualize your rhythm?", + "preview_placeholder_desc": "Enter a GitHub username above to instantly generate your streak badge.", + "user_not_found": "GitHub user not found", + "user_not_found_desc": "Please check the username and try again.", + "preview_auto_theme": "Random theme changes on every page load and disables caching", + "preview_caching_tip": "Preview updates on every change. Hosted badge is cached at UTC midnight", + "preview_empty_tip": "Add a username to enable live preview and export snippets", + "features": { + "sync_title": "Real-time Sync", + "sync_desc": "Pulled directly from GitHub GraphQL API. Your streak updates as fast as your code pushes.", + "theme_title": "Theme Engine", + "theme_desc": "Switch between Neon, Dracula, or custom HEX modes via simple URL management.", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." + }, + "generate_badge": "Generate Badge", + "verified_profile": "Verified Profile", + "verifying": "Verifying...", + "preview_monolith": "PREVIEW MONOLITH", + "interactive_preview_title": "Interactive Monolith Preview", + "interactive_preview_desc": "CommitPulse compiles your public GitHub contribution history into a customizable 3D city. The taller the towers, the more you committed that day. Enter a GitHub username above to instantly generate your streak badge.", + "input_aria_label": "Enter GitHub username to generate badge", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "Your Monolith is Ready - Deploy It in 4 Steps", + "markdown_copied": "Markdown Copied", + "dismiss_aria": "Dismiss guide", + "step_1_title": "Open Your Profile Repo", + "step_1_body": "Navigate to github.com/YOUR_USERNAME/YOUR_USERNAME - your special profile repository.", + "step_2_title": "Edit README.md", + "step_2_body": "Click the pencil icon to open the file in GitHub's built-in editor.", + "step_3_title": "Paste the Snippet", + "step_3_body": "Place your cursor wherever you want the monolith to appear, then paste (Ctrl+V / Cmd+V).", + "step_4_title": "Save & Ship It", + "step_4_body": "Click \"Commit changes\" and visit your profile. Your 3D streak is now live.", + "copied_snippet_label": "Your copied snippet", + "color_tip": "Tip: Add ?accent=808080 to the URL to change your monolith's colour palette.", + "watch_dashboard_btn": "Watch Your Dashboard", + "customize_more_btn": "Want to customize more?" + }, + "customize_cta": { + "studio_badge": "Customization Studio", + "title": "Want to fine-tune your monolith?", + "desc": "Dial in every pixel — swap accent colors, try a dark or neon theme, toggle logarithmic height scaling, and preview changes live before you paste a single line.", + "btn": "Open Customization Studio" + }, + "customize": { + "back_to_home": "Back to Home", + "title": "Fine-tune your monolith.", + "desc": "Every change below updates the preview in real-time. Copy the export snippet when you're done. No extra steps required.", + "live_preview": "Live Preview", + "active_params": "Active Parameters", + "empty_preview_desc": "The live badge preview will appear here once a username is added.", + "controls": { + "username": "GitHub Username", + "username_placeholder": "Enter username...", + "select_theme": "Select Theme", + "theme_placeholder": "Select Preset Theme", + "theme_mode": "Theme Mode", + "custom_bg": "Custom Background", + "custom_accent": "Custom Accent", + "custom_text": "Custom Text", + "log_scaling": "Logarithmic Height Scaling", + "speed": "Animation Speed", + "radius": "Corner Radius", + "badge_size": "Badge Size", + "sync_year": "Sync Year", + "clear_custom": "Clear Custom Colors", + "theme_presets": "Theme Preset", + "color_overrides": "Custom Color Overrides", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "Export Snippet", + "snippet_desc": "Switch formats without changing the live badge configuration.", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", + "copy_format": "Copy {{format}}", + "copied": "Copied!", + "copy_aria_disabled": "Add a GitHub username to enable copying the {{format}} export snippet", + "copy_aria_enabled": "Copy {{format}} export snippet to clipboard", + "footer_tip": "The badge renders server-side, no script required.", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "Generate Your Own Dashboard", + "refresh_btn": "Refresh Data", + "refreshing": "Refreshing...", + "refreshed_toast": "Dashboard refreshed successfully", + "profile": { + "score": "Developer Score", + "repos": "Repositories", + "followers": "Followers", + "following": "Following", + "stars": "Stars", + "joined": "Joined", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" + }, + "achievements": { + "title": "Achievements", + "see_all": "See All Achievements", + "show_less": "Show Less" + }, + "activity": { + "title": "Activity Landscape", + "year": "Year", + "month": "Month", + "week": "Week", + "commits": "Commits", + "intensity": "Commit Intensity", + "loc": "Lines of Code", + "loc_desc": "Lines of code modified over time", + "commits_desc": "Commit frequency over time", + "lines_modified": "{{count}} lines modified", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "Top Languages", + "primary": "Primary Language", + "no_data": "No language data found" + }, + "clock": { + "title": "Commit Clock", + "active_days": "Weekly activity cycle", + "tooltip_no_commits": "No commits recorded for this weekday", + "tooltip_peak": "Peak weekday in this cycle", + "tooltip_activity": "Weekly activity point" + }, + "heatmap": { + "title": "Contribution Heatmap", + "less": "Less", + "more": "More", + "last_365": "Last 365 days", + "tooltip_single": "{{count}} contribution on {{date}}", + "tooltip_plural": "{{count}} contributions on {{date}}", + "no_activity": "No activity recorded", + "peak_activity": "Peak activity day", + "high_activity": "High activity day", + "steady_contribution": "Steady contribution day", + "light_activity": "Light activity day", + "no_active_streak": "No active streak", + "active_streak": "{{streak}}-day active streak", + "empty": "No recent activity to display", + "code_activity": "Code activity recorded", + "no_code_changes": "No code changes recorded" + }, + "insights": { + "title": "AI Insights" + }, + "stats": { + "current_streak": "Current Streak", + "peak_streak": "Peak Streak", + "contributions": "Contributions", + "days": "Days", + "last_year": "Last Year", + "utc_disclaimer": "Streak calculated in UTC timezone" + }, + "share": { + "title": "Share Pulse", + "close_aria": "Close share panel", + "copy_link": "Copy Link", + "copy_link_desc": "Copy your profile URL to clipboard", + "link_copied": "Link copied", + "share_x": "Share on X", + "share_x_desc": "Tweet your pulse to the world", + "share_linkedin": "LinkedIn", + "share_linkedin_desc": "Post your dev activity to your network", + "copy_markdown": "Copy README Markdown", + "copy_markdown_desc": "Copy markdown snippet for your README", + "download_png": "Download PNG Snapshot", + "download_png_desc": "Save a snapshot of your dashboard", + "download_svg": "Download Vector SVG Monolith", + "download_svg_desc": "Download the raw monolith SVG", + "download_json": "Export Structured JSON Data", + "download_json_desc": "Export raw streak and language data", + "share_os": "System Share", + "share_os_desc": "AirDrop, WhatsApp, Messages & more", + "share_os_fallback": "More Options", + "share_os_fallback_desc": "Open the system share dialog", + "share_reddit": "Reddit", + "share_reddit_desc": "Share on Reddit", + "downloaded": "Saved Asset!", + "svg_downloaded": "SVG Downloaded!", + "json_downloaded": "JSON Downloaded!", + "failed": "Failed — try again", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "Download Printable 3D STL" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Contribution Forecast", + "subtitle": "Predict future growth based on your coding velocity and trend slopes.", + "weekly_velocity": "Weekly Velocity", + "monthly_velocity": "Monthly Velocity", + "projected_month": "Projected Month-End", + "projected_year": "Projected Year-End", + "consistency_rating": "Consistency Rating", + "trend_direction": "Trend", + "commits_per_week": "{{count}} Commits/Week", + "commits_per_month": "{{count}} Commits/Month", + "commits": "{{count}} Commits", + "growth_rate": "Growth Rate: {{rate}} Commits/Day²", + "consistency_level": { + "elite": "Elite (Very Consistent)", + "consistent": "High (Consistent)", + "occasional": "Moderate (Occasional)", + "sporadic": "Low (Sporadic)", + "inactive": "Inactive" + }, + "trends": { + "strong_growth": "Strong Growth", + "moderate_growth": "Moderate Growth", + "stable": "Stable Rhythm", + "cooling": "Cooling", + "decline": "Significant Decline" + }, + "no_activity": "No past activity data available to generate predictions." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/es.json b/scratch/es.json new file mode 100644 index 000000000..58d665a8c --- /dev/null +++ b/scratch/es.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "Ir al inicio", + "repo": "Repositorio de GitHub", + "contributors": "Colaboradores", + "theme_toggle": "Cambiar tema", + "menu_open": "Abrir menú", + "menu_close": "Cerrar menú", + "compare": "Comparar", + "customization_studio": "Estudio de Personalización", + "generator": "Generador", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "Diseñado para la comunidad de desarrolladores de élite.", + "contributors": "Colaboradores", + "documentation": "Documentación", + "creator": "Creador", + "copyright": "© {{year}} CommitPulse. Todos los derechos reservados.", + "navigation": "Navegación", + "resources": "Recursos", + "connect": "Conectar", + "made_with": "Hecho con ❤️ para desarrolladores", + "home": "Inicio", + "compare": "Comparar", + "customization": "Personalización", + "github_repo": "Repositorio de GitHub", + "github": "GitHub", + "creator_github": "Creador en GitHub", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "Generador" + }, + "landing": { + "empty_username_warning": "Ingresa un usuario de GitHub para copiar el enlace de tu insignia.", + "max_length_warning": "Límite de usuario de GitHub alcanzado (máximo 39 caracteres)", + "title": "Eleva tu historia\nde {contribuciones}.", + "subtitle": "No te conformes con cuadrículas planas. Genera monolitos 3D isométricos de alta fidelidad que visualizan tu ritmo de programación con precisión profesional.", + "watch_dashboard": "Ver panel", + "discord_community": "Únete a la comunidad en Discord", + "input_placeholder": "Ingresa el usuario de GitHub", + "clear_input": "Limpiar entrada", + "copy_link": "Copiar enlace", + "copied": "Copiado", + "recent": "Recientes:", + "clear": "Limpiar", + "preview_placeholder_title": "¿Listo para visualizar tu ritmo?", + "preview_placeholder_desc": "Ingresa un nombre de usuario de GitHub arriba para generar instantáneamente tu insignia de racha.", + "user_not_found": "Usuario de GitHub no encontrado", + "user_not_found_desc": "Por favor, verifica el nombre de usuario e intenta de nuevo.", + "preview_auto_theme": "El tema aleatorio cambia en cada carga de página y desactiva el almacenamiento en caché", + "preview_caching_tip": "La previsualización se actualiza con cada cambio. La insignia se almacena en caché a la medianoche UTC", + "preview_empty_tip": "Agrega un usuario para habilitar la previsualización en vivo y los fragmentos de exportación", + "features": { + "sync_title": "Sincronización en tiempo real", + "sync_desc": "Obtenido directamente de la API GraphQL de GitHub. Tu racha se actualiza tan rápido como envías tu código.", + "theme_title": "Motor de temas", + "theme_desc": "Cambia entre Neon, Dracula o modos HEX personalizados mediante una administración de URL sencilla.", + "isometric_title": "Matemática isométrica", + "isometric_desc": "Sofisticadas fórmulas de proyección 3D convierten datos 2D en arquitectura digital." + }, + "generate_badge": "Generar insignia", + "verified_profile": "Perfil verificado", + "verifying": "Verificando...", + "preview_monolith": "MONOLITO DE PREVISUALIZACIÓN", + "interactive_preview_title": "Previsualización interactiva del monolito", + "interactive_preview_desc": "CommitPulse compila tu historial de contribuciones públicas de GitHub en una ciudad 3D customizable. Cuanto más altas son las torres, más commits hiciste ese dá. Ingresa un nombre de usuario de GitHub arriba para generar instantáneamente tu insignia.", + "input_aria_label": "Ingresa el usuario de GitHub para generar la insignia", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "Tu monolito está listo - Despliégalo en 4 pasos", + "markdown_copied": "Markdown copiado", + "dismiss_aria": "Dismiss guide", + "step_1_title": "Abre el repositorio de tu perfil", + "step_1_body": "Navega a github.com/TU_USUARIO/TU_USUARIO - tu repositorio de perfil especial.", + "step_2_title": "Edita el README.md", + "step_2_body": "Haz clic en el icono del lápiz para abrir el archivo en el editor integrado de GitHub.", + "step_3_title": "Pega el fragmento", + "step_3_body": "Coloca el cursor donde quieras que aparezca el monolito, luego pega (Ctrl+V / Cmd+V).", + "step_4_title": "Guarda y publica", + "step_4_body": "Haz clic en \"Commit changes\" y visita tu perfil. Tu racha 3D ya está activa.", + "copied_snippet_label": "Tu fragmento copiado", + "color_tip": "Consejo: Agrega ?accent=808080 a la URL para cambiar la paleta de colores de tu monolito.", + "watch_dashboard_btn": "Ver tu panel", + "customize_more_btn": "¿Quieres personalizar más?" + }, + "customize_cta": { + "studio_badge": "Estudio de personalización", + "title": "¿Quieres ajustar tu monolito?", + "desc": "Ajusta cada píxel: cambia los colores de acento, prueba un tema oscuro o neón, activa la escala de altura logarítmica y previsualiza los cambios en vivo antes de pegar una sola línea.", + "btn": "Abrir estudio de personalización" + }, + "customize": { + "back_to_home": "Volver al inicio", + "title": "Ajusta tu monolito.", + "desc": "Cada cambio a continuación actualiza la previsualización en tiempo real. Copia el fragmento de exportación cuando termines. Sin pasos adicionales.", + "live_preview": "Previsualización en vivo", + "active_params": "Parámetros activos", + "empty_preview_desc": "La previsualización en vivo de la insignia aparecerá aquí una vez que se agregue un usuario.", + "controls": { + "username": "Usuario de GitHub", + "username_placeholder": "Ingresa usuario...", + "select_theme": "Seleccionar tema", + "theme_placeholder": "Seleccionar tema preestablecido", + "theme_mode": "Modo de tema", + "custom_bg": "Fondo personalizado", + "custom_accent": "Acento personalizado", + "custom_text": "Texto personalizado", + "log_scaling": "Escala de altura logarítmica", + "speed": "Velocidad de animación", + "radius": "Radio de esquina", + "badge_size": "Tamaño de insignia", + "sync_year": "Año de sincronización", + "clear_custom": "Limpiar colores personalizados", + "theme_presets": "Tema preestablecido", + "color_overrides": "Colores personalizados", + "font": "Fuente", + "custom_font_option": "Fuente de Google personalizada...", + "custom_font_placeholder": "ej. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "Fragmento de exportación", + "snippet_desc": "Cambia de formato sin alterar la configuración de la insignia en vivo.", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Descargar insignia", + "downloading": "Descargando...", + "download_not_available": "Descarga no disponible", + "copy_workflow": "Copiar flujo de trabajo", + "copy_format": "Copiar {{format}}", + "copied": "¡Copiado!", + "copy_aria_disabled": "Agrega un usuario de GitHub para habilitar la copia del fragmento de exportación {{format}}", + "copy_aria_enabled": "Copiar fragmento de exportación {{format}} al portapapeles", + "footer_tip": "Pega esto en el README.md de tu perfil de GitHub. La insignia se genera en el servidor, no requiere scripts.", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "Generar tu propio panel", + "refresh_btn": "Actualizar datos", + "refreshing": "Actualizando...", + "refreshed_toast": "Panel actualizado con éxito", + "profile": { + "score": "Puntaje de desarrollador", + "repos": "Repositorios", + "followers": "Seguidores", + "following": "Siguiendo", + "stars": "Estrellas", + "joined": "Unido en", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Compartir tu Pulse" + }, + "achievements": { + "title": "Logros", + "see_all": "Ver todos los logros", + "show_less": "Ver menos" + }, + "activity": { + "title": "Paisaje de actividad", + "year": "Año", + "month": "Mes", + "week": "Semana", + "commits": "Commits", + "intensity": "Intensidad de commits", + "loc": "Líneas de código", + "loc_desc": "Líneas de código modificadas a lo largo del tiempo", + "commits_desc": "Frecuencia de commits a lo largo del tiempo", + "lines_modified": "{{count}} líneas modificadas", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "Idiomas principales", + "primary": "Idioma principal", + "no_data": "No se encontraron datos de idiomas" + }, + "clock": { + "title": "Reloj de commits", + "active_days": "Ciclo de actividad semanal", + "tooltip_no_commits": "No hay commits registrados para este día de la semana", + "tooltip_peak": "Día de la semana de mayor actividad en este ciclo", + "tooltip_activity": "Punto de actividad semanal" + }, + "heatmap": { + "title": "Mapa de calor de contribuciones", + "less": "Menos", + "more": "Más", + "last_365": "Últimos 365 días", + "tooltip_single": "{{count}} contribución el {{date}}", + "tooltip_plural": "{{count}} contribuciones el {{date}}", + "no_activity": "Sin actividad registrada", + "peak_activity": "Día de actividad máxima", + "high_activity": "Día de alta actividad", + "steady_contribution": "Día de contribución constante", + "light_activity": "Día de actividad ligera", + "no_active_streak": "Sin racha activa", + "active_streak": "Racha activa de {{streak}} días", + "empty": "Sin actividad reciente para mostrar", + "code_activity": "Actividad de código registrada", + "no_code_changes": "Sin cambios de código registrados" + }, + "insights": { + "title": "Ideas de IA" + }, + "stats": { + "current_streak": "Racha actual", + "peak_streak": "Racha máxima", + "contributions": "Contribuciones", + "days": "Días", + "last_year": "El año pasado", + "utc_disclaimer": "Racha calculada en la zona horaria UTC" + }, + "share": { + "title": "Compartir racha", + "close_aria": "Cerrar panel de opciones para compartir", + "copy_link": "Copiar enlace", + "copy_link_desc": "Copiar URL de tu perfil al portapapeles", + "link_copied": "¡Enlace copiado!", + "share_x": "Compartir en X", + "share_x_desc": "Publica tu racha en X", + "share_linkedin": "Compartir en LinkedIn", + "share_linkedin_desc": "Publica tu actividad de desarrollo en tu red", + "copy_markdown": "Copiar Markdown", + "copy_markdown_desc": "Copiar fragmento Markdown para tu README", + "download_png": "Descargar PNG", + "download_png_desc": "Guardar una captura de tu panel", + "download_svg": "Descargar SVG", + "download_svg_desc": "Descargar el archivo SVG original del monolito", + "download_json": "Descargar JSON", + "download_json_desc": "Exportar datos brutos de racha e idiomas", + "share_os": "Compartir mediante OS Sheet", + "share_os_desc": "AirDrop, WhatsApp, Mensajes y más", + "share_os_fallback": "Más opciones", + "share_os_fallback_desc": "Abrir el cuadro de diálogo para compartir del sistema", + "share_reddit": "Reddit", + "share_reddit_desc": "Compartir en Reddit", + "downloaded": "¡Descargado!", + "svg_downloaded": "¡SVG descargado!", + "json_downloaded": "¡JSON descargado!", + "failed": "Error — intenta de nuevo", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "Descargar monolito 3D imprimible STL" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Pronóstico de Contribuciones", + "subtitle": "Prediga el crecimiento futuro basado en su velocidad de programación y pendientes de tendencia.", + "weekly_velocity": "Velocidad Semanal", + "monthly_velocity": "Velocidad Mensual", + "projected_month": "Fin de Mes Proyectado", + "projected_year": "Fin de Año Proyectado", + "consistency_rating": "Calificación de Consistencia", + "trend_direction": "Tendencia", + "commits_per_week": "{{count}} commits/sem", + "commits_per_month": "{{count}} commits/mes", + "commits": "{{count}} commits", + "growth_rate": "Tasa de Crecimiento: {{rate}} commits/día²", + "consistency_level": { + "elite": "Élite (Muy Consistente)", + "consistent": "Alta (Consistente)", + "occasional": "Moderada (Ocasional)", + "sporadic": "Baja (Esporádica)", + "inactive": "Inactivo" + }, + "trends": { + "strong_growth": "Crecimiento Fuerte", + "moderate_growth": "Crecimiento Moderado", + "stable": "Ritmo Estable", + "cooling": "Enfriamiento", + "decline": "Disminución Significativa" + }, + "no_activity": "No hay datos de actividad pasada disponibles para generar predicciones." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/fr.json b/scratch/fr.json new file mode 100644 index 000000000..1f0c860ab --- /dev/null +++ b/scratch/fr.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "Retour à l'accueil", + "repo": "Dépôt GitHub", + "contributors": "Contributeurs", + "theme_toggle": "Changer de thème", + "menu_open": "Ouvrir le menu", + "menu_close": "Fermer le menu", + "compare": "Comparer", + "customization_studio": "Studio de Personnalisation", + "generator": "Générateur", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "Conçu pour la communauté des développeurs d'élite.", + "contributors": "Contributeurs", + "documentation": "Documentation", + "creator": "Créateur", + "copyright": "© {{year}} CommitPulse. Tous droits réservés.", + "navigation": "Navigation", + "resources": "Ressources", + "connect": "Connecter", + "made_with": "Fait avec ❤️ pour les développeurs", + "home": "Accueil", + "compare": "Comparer", + "customization": "Personnalisation", + "github_repo": "Dépôt GitHub", + "github": "GitHub", + "creator_github": "Créateur sur GitHub", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "Générateur" + }, + "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", + "title": "Sublimez Votre\nHistoire de {Contributions}.", + "subtitle": "Ne vous contentez plus de grilles plates. Générez des monolithes 3D isométriques haute fidélité qui visualisent votre rythme de codage avec une précision professionnelle.", + "watch_dashboard": "Voir le tableau de bord", + "discord_community": "Rejoindre la communauté sur Discord", + "input_placeholder": "Entrez votre nom d'utilisateur GitHub", + "clear_input": "Effacer la saisie", + "copy_link": "Copier le lien", + "copied": "Copié", + "recent": "Récent:", + "clear": "Effacer", + "preview_placeholder_title": "Prêt à visualiser votre rythme ?", + "preview_placeholder_desc": "Entrez un nom d'utilisateur GitHub ci-dessus pour générer instantanément votre badge de streak.", + "user_not_found": "Utilisateur GitHub non trouvé", + "user_not_found_desc": "Veuillez vérifier le nom d'utilisateur et réessayer.", + "preview_auto_theme": "Le thème aléatoire change à chaque chargement de page et désactive le cache", + "preview_caching_tip": "L'aperçu se met à jour à chaque modification. Le badge hébergé est mis en cache à minuit UTC", + "preview_empty_tip": "Ajoutez un nom d'utilisateur pour activer l'aperçu en direct et les extraits d'export", + "features": { + "sync_title": "Synchronisation en temps réel", + "sync_desc": "Extrait directement de l'API GraphQL de GitHub. Votre série de commits se met à jour aussi vite que vous poussez votre code.", + "theme_title": "Moteur de Thèmes", + "theme_desc": "Basculez entre les modes Néon, Dracula ou HEX personnalisés via une gestion simple des URL.", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." + }, + "generate_badge": "Générer le badge", + "verified_profile": "Profil vérifié", + "verifying": "Vérification...", + "preview_monolith": "APERCU DU MONOLITHE", + "interactive_preview_title": "Aperçu interactif du monolithe", + "interactive_preview_desc": "CommitPulse compile votre historique de contributions GitHub publiques dans une ville 3D personnalisable. Plus les tours sont hautes, plus vous avez contribué ce jour-là. Entrez un nom d'utilisateur GitHub ci-dessus pour générer instantanément votre badge de streak.", + "input_aria_label": "Entrez le nom d'utilisateur GitHub pour générer le badge", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "Votre Monolithe est Prêt - Déployez-le en 4 étapes", + "markdown_copied": "Markdown copié", + "dismiss_aria": "Dismiss guide", + "step_1_title": "Ouvrez le dépôt de votre profil", + "step_1_body": "Rendez-vous sur github.com/VOTRE_PSEUDO/VOTRE_PSEUDO - votre dépôt de profil spécial.", + "step_2_title": "Modifiez le fichier README.md", + "step_2_body": "Cliquez sur l'icône de crayon pour ouvrir le fichier dans l'éditeur intégré de GitHub.", + "step_3_title": "Collez l'extrait", + "step_3_body": "Placez votre curseur là où vous souhaitez que le monolithe apparaisse, puis collez (Ctrl+V / Cmd+V).", + "step_4_title": "Enregistrez et publiez", + "step_4_body": "Cliquez sur \"Commit changes\" et visitez votre profil. Votre série 3D est désormais en ligne.", + "copied_snippet_label": "Votre extrait copié", + "color_tip": "Astuce : Ajoutez ?accent=808080 à l'URL pour changer la palette de couleurs de votre monolithe.", + "watch_dashboard_btn": "Voir votre tableau de bord", + "customize_more_btn": "Envie de personnaliser davantage ?" + }, + "customize_cta": { + "studio_badge": "Studio de Personnalisation", + "title": "Envie de peaufiner votre monolithe ?", + "desc": "Ajustez chaque pixel — modifiez les couleurs d'accentuation, essayez un thème sombre ou néon, activez l'échelle de hauteur logarithmique et prévisualisez les modifications en direct avant de coller la moindre ligne.", + "btn": "Ouvrir le Studio de Personnalisation" + }, + "customize": { + "back_to_home": "Retour à l'accueil", + "title": "Peaufinez votre monolithe.", + "desc": "Chaque modification ci-dessous met à jour l'aperçu en temps réel. Copiez l'extrait d'exportation lorsque vous avez terminé. Aucune étape supplémentaire requise.", + "live_preview": "Aperçu en direct", + "active_params": "Paramètres actifs", + "empty_preview_desc": "L'aperçu en direct du badge apparaîtra ici une fois qu'un nom d'utilisateur aura été ajouté.", + "controls": { + "username": "Nom d'utilisateur GitHub", + "username_placeholder": "Entrez le nom d'utilisateur...", + "select_theme": "Sélectionner le thème", + "theme_placeholder": "Sélectionner un thème prédéfini", + "theme_mode": "Mode thématique", + "custom_bg": "Arrière-plan personnalisé", + "custom_accent": "Accentuation personnalisée", + "custom_text": "Texte personnalisé", + "log_scaling": "Échelle de hauteur logarithmique", + "speed": "Vitesse d'animation", + "radius": "Rayon des coins", + "badge_size": "Taille du badge", + "sync_year": "Année de synchronisation", + "clear_custom": "Effacer les couleurs personnalisées", + "theme_presets": "Préréglage de thème", + "color_overrides": "Surcharges de couleurs personnalisées", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "Extrait d'exportation", + "snippet_desc": "Basculez entre les formats sans modifier la configuration du badge en direct.", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", + "copy_format": "Copier le {{format}}", + "copied": "Copié !", + "copy_aria_disabled": "Ajoutez un nom d'utilisateur GitHub pour activer la copie de l'extrait d'exportation {{format}}", + "copy_aria_enabled": "Copier l'extrait d'exportation {{format}} dans le presse-papiers", + "footer_tip": "Collez ceci dans le fichier README.md de votre profil GitHub. Le badge est généré côté serveur, aucun script n'est requis.", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "Générer votre propre tableau de bord", + "refresh_btn": "Actualiser les données", + "refreshing": "Actualisation...", + "refreshed_toast": "Tableau de bord actualisé avec succès", + "profile": { + "score": "Score du développeur", + "repos": "Dépôts", + "followers": "Abonnés", + "following": "Abonnements", + "stars": "Étoiles", + "joined": "Rejoint en", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" + }, + "achievements": { + "title": "Succès", + "see_all": "Voir tous les succès", + "show_less": "Voir moins" + }, + "activity": { + "title": "Activité globale", + "year": "Année", + "month": "Mois", + "week": "Semaine", + "commits": "Commits", + "intensity": "Intensité des commits", + "loc": "Lignes de code", + "loc_desc": "Lignes de code modifiées au fil du temps", + "commits_desc": "Fréquence des commits au fil du temps", + "lines_modified": "{{count}} lignes modifiées", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "Principales langues", + "primary": "Langue principale", + "no_data": "Aucune donnée de langue trouvée" + }, + "clock": { + "title": "Horloge des commits", + "active_days": "Cycle d'activité hebdomadaire", + "tooltip_no_commits": "Aucun commit enregistré pour ce jour de la semaine", + "tooltip_peak": "Jour de la semaine le plus actif de ce cycle", + "tooltip_activity": "Point d'activité hebdomadaire" + }, + "heatmap": { + "title": "Carte thermique des contributions", + "less": "Moins", + "more": "Plus", + "last_365": "365 derniers jours", + "tooltip_single": "{{count}} contribution le {{date}}", + "tooltip_plural": "{{count}} contributions le {{date}}", + "no_activity": "Aucune activité enregistrée", + "peak_activity": "Journée d'activité maximale", + "high_activity": "Journée de haute activité", + "steady_contribution": "Journée de contribution régulière", + "light_activity": "Journée d'activité légère", + "no_active_streak": "Aucune série active", + "active_streak": "Série active de {{streak}} jours", + "empty": "Aucune activité récente à afficher", + "code_activity": "Activité de code enregistrée", + "no_code_changes": "Aucun changement de code enregistré" + }, + "insights": { + "title": "Analyses de l'IA" + }, + "stats": { + "current_streak": "Série actuelle", + "peak_streak": "Série maximale", + "contributions": "Contributions", + "days": "Jours", + "last_year": "L'année dernière", + "utc_disclaimer": "Série de commits calculée dans le fuseau horaire UTC" + }, + "share": { + "title": "Partager l'activité", + "close_aria": "Fermer le panneau des options de partage", + "copy_link": "Copier le lien", + "copy_link_desc": "Copier l'URL de votre profil dans le presse-papiers", + "link_copied": "Lien copié !", + "share_x": "Partager sur X", + "share_x_desc": "Tweetez votre activité sur X", + "share_linkedin": "Partager sur LinkedIn", + "share_linkedin_desc": "Publiez votre activité de dev sur votre réseau", + "copy_markdown": "Copier le Markdown", + "copy_markdown_desc": "Copier l'extrait Markdown pour votre README", + "download_png": "Télécharger en PNG", + "download_png_desc": "Enregistrer une capture de votre tableau de bord", + "download_svg": "Télécharger l'SVG", + "download_svg_desc": "Télécharger l'SVG brut du monolithe", + "download_json": "Télécharger le JSON", + "download_json_desc": "Exporter les données brutes d'activité et de langues", + "share_os": "Partager via la feuille système", + "share_os_desc": "AirDrop, WhatsApp, Messages et plus", + "share_os_fallback": "Plus d'options", + "share_os_fallback_desc": "Ouvrir la boîte de dialogue de partage système", + "share_reddit": "Reddit", + "share_reddit_desc": "Partager sur Reddit", + "downloaded": "Téléchargé !", + "svg_downloaded": "SVG téléchargé !", + "json_downloaded": "JSON téléchargé !", + "failed": "Échec — réessayez", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "Télécharger le monolithe STL 3D imprimable" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Prévisions de Contribution", + "subtitle": "Prédisez la croissance future en fonction de votre vitesse de codage et des pentes de tendance.", + "weekly_velocity": "Vitesse Hebdomadaire", + "monthly_velocity": "Vitesse Mensuelle", + "projected_month": "Fin de Mois Projetée", + "projected_year": "Fin d'Année Projetée", + "consistency_rating": "Indice de Cohérence", + "trend_direction": "Tendance", + "commits_per_week": "{{count}} commits/semaine", + "commits_per_month": "{{count}} commits/mois", + "commits": "{{count}} commits", + "growth_rate": "Taux de Croissance: {{rate}} commits/jour²", + "consistency_level": { + "elite": "Élite (Très Cohérent)", + "consistent": "Élevé (Cohérent)", + "occasional": "Modéré (Occasionnel)", + "sporadic": "Faible (Sporadique)", + "inactive": "Inactif" + }, + "trends": { + "strong_growth": "Forte Croissance", + "moderate_growth": "Croissance Modérée", + "stable": "Rythme Stable", + "cooling": "Ralentissement", + "decline": "Baisse Significative" + }, + "no_activity": "Aucune donnée d'activité passée disponible pour générer des prévisions." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/hi.json b/scratch/hi.json new file mode 100644 index 000000000..4e14a68c2 --- /dev/null +++ b/scratch/hi.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "होम पर जाएं", + "repo": "गिटहब रेपॉजिटरी", + "contributors": "योगदानकर्ता", + "theme_toggle": "थीम बदलें", + "menu_open": "मेन्यू खोलें", + "menu_close": "मेन्यू बंद करें", + "compare": "तुलना करें", + "customization_studio": "कस्टमाइजेशन स्टूडियो", + "generator": "जनरेटर", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "उत्कृष्ट डेवलपर समुदाय के लिए डिज़ाइन किया गया।", + "contributors": "योगदानकर्ता", + "documentation": "दस्तावेज़", + "creator": "निर्माता", + "copyright": "© {{year}} CommitPulse. सभी अधिकार सुरक्षित।", + "navigation": "नेविगेशन", + "resources": "संसाधन", + "connect": "जुड़ें", + "made_with": "डेवलपर्स के लिए ❤️ के साथ बनाया गया", + "home": "होम", + "compare": "तुलना करें", + "customization": "कस्टमाइजेशन", + "github_repo": "गिटहब रिपोजिटरी", + "github": "गिटहब", + "creator_github": "गिटहब पर निर्माता", + "discord": "डिस्कॉर्ड", + "twitter": "ट्विटर", + "linkedin": "लिंक्डइन", + "generator": "जनरेटर" + }, + "landing": { + "empty_username_warning": "बैज लिंक कॉपी करने के लिए कृपया गिटहब यूजरनेम दर्ज करें।", + "max_length_warning": "गिटहब यूजरनेम सीमा समाप्त (अधिकतम 39 वर्ण)", + "title": "अपनी {योगदान} कहानी\nको बेहतर बनाएं।", + "subtitle": "फ्लैट ग्रिड्स से समझौता न करें। उच्च-गुणवत्ता वाले, 3D आइसोमेट्रिक मोनोलिथ उत्पन्न करें जो आपके कोडिंग लय को पेशेवर सटीकता के साथ दिखाते हैं।", + "watch_dashboard": "डैशबोर्ड देखें", + "discord_community": "डिसकॉर्ड पर कम्युनिटी में शामिल हों", + "input_placeholder": "गिटहब यूजरनेम दर्ज करें", + "clear_input": "इनपुट साफ़ करें", + "copy_link": "लिंक कॉपी करें", + "copied": "कॉपी किया गया", + "recent": "हालिया:", + "clear": "साफ़ करें", + "preview_placeholder_title": "क्या आप अपनी गति को देखने के लिए तैयार हैं?", + "preview_placeholder_desc": "अपनी स्ट्रीक बैज को तुरंत उत्पन्न करने के लिए ऊपर एक GitHub उपयोगकर्ता नाम दर्ज करें।", + "user_not_found": "GitHub उपयोगकर्ता नहीं मिला", + "user_not_found_desc": "कृपया उपयोगकर्ता नाम जांचें और पुनः प्रयास करें।", + "preview_auto_theme": "रैंडम थीम प्रत्येक पृष्ठ लोड पर बदलती है और कैशिंग को अक्षम करती है", + "preview_caching_tip": "पूर्वावलोकन प्रत्येक परिवर्तन पर अपडेट होता है। होस्ट किया गया बैज UTC मध्यरात्रि में कैश किया जाता है", + "preview_empty_tip": "लाइव पूर्वावलोकन और निर्यात स्निपेट सक्षम करने के लिए यूजरनेम जोड़ें", + "features": { + "sync_title": "रीयल-टाइम सिंक", + "sync_desc": "सीधे गिटहब ग्राफक्यूएल एपीआई से प्राप्त। जैसे ही आप कोड पुश करते हैं, आपकी स्ट्रीक अपडेट हो जाती है।", + "theme_title": "थीम इंजन", + "theme_desc": "सरल यूआरएल प्रबंधन के माध्यम से नियॉन, ड्रैकुला, या कस्टम हेक्स मोड के बीच स्विच करें।", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." + }, + "generate_badge": "बैज उत्पन्न करें", + "verified_profile": "सत्यापित प्रोफ़ाइल", + "verifying": "सत्यापित किया जा रहा है...", + "preview_monolith": "प्रोटोकॉल मोनोलिथ", + "interactive_preview_title": "इंटरएक्टिव मोनोलिथ पूर्वावलोकन", + "interactive_preview_desc": "CommitPulse आपके सार्वजनिक GitHub योगदान इतिहास को एक अनुकूलन योग्य 3D शहर में संकलित करता है। टावर जितने ऊंचे होंगे, आपने उस दिन उतने ही अधिक कमिट किए॥ अपनी स्ट्रीक बैज को त्रुत उत्पन्न करने के लिए ऊपर एक GitHub उपभोक्ता नाम दर्ज करें॥", + "input_aria_label": "बैज जनरेट करने के लिए गिटहब यूजरनेम दर्ज करें", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "आपका मोनोलिथ तैयार है - इसे 4 चरणों में परिनियोजित करें", + "markdown_copied": "मार्कडाउन कॉपी किया गया", + "dismiss_aria": "Dismiss guide", + "step_1_title": "अपनी प्रोफ़ाइल रेपो खोलें", + "step_1_body": "github.com/YOUR_USERNAME/YOUR_USERNAME पर जाएं - आपकी विशेष प्रोफ़ाइल रिपॉजिटरी।", + "step_2_title": "README.md संपादित करें", + "step_2_body": "गिटहब के अंतर्निहित संपादक में फ़ाइल खोलने के लिए पेंसिल आइकन पर क्लिक करें।", + "step_3_title": "स्निपेट पेस्ट करें", + "step_3_body": "जहां आप मोनोलिथ दिखाना चाहते हैं वहां अपना कर्सर रखें, फिर पेस्ट करें (Ctrl+V / Cmd+V)।", + "step_4_title": "सहेजें और प्रकाशित करें", + "step_4_body": "\"Commit changes\" पर क्लिक करें और अपनी प्रोफ़ाइल पर जाएं। आपकी 3D स्ट्रीक अब लाइव है।", + "copied_snippet_label": "आपका कॉपी किया गया स्निपेट", + "color_tip": "टिप: अपने मोनोलिथ के रंग पैलेट को बदलने के लिए यूआरएल में ?accent=808080 जोड़ें।", + "watch_dashboard_btn": "अपना डैशबोर्ड देखें", + "customize_more_btn": "क्या आप और कस्टमाइज़ करना चाहते हैं?" + }, + "customize_cta": { + "studio_badge": "कस्टमाइज़ेशन स्टूडियो", + "title": "अपने मोनोलिथ को फाइन-ट्यून करना चाहते हैं?", + "desc": "हर पिक्सेल को सेट करें - एक्सेंट रंग बदलें, डार्क या नियॉन थीम आज़माएं, लॉगरिदमिक हाइट स्केलिंग चालू करें, और सिंगल लाइन पेस्ट करने से पहले लाइव बदलाव देखें।", + "btn": "कस्टमाइज़ेशन स्टूडियो खोलें" + }, + "customize": { + "back_to_home": "होम पर वापस जाएं", + "title": "अपने मोनोलिथ को फाइन-ट्यून करें।", + "desc": "नीचे दिया गया हर बदलाव लाइव पूर्वावलोकन को रीयल-टाइम में अपडेट करता है। काम पूरा होने पर एक्सपोर्ट स्निपेट कॉपी करें। कोई अतिरिक्त कदम नहीं।", + "live_preview": "लाइव पूर्वावलोकन", + "active_params": "सक्रिय पैरामीटर", + "empty_preview_desc": "यूज़रनेम जोड़ने के बाद लाइव बैज पूर्वावलोकन यहां दिखाई देगा।", + "controls": { + "username": "गिटहब यूजरनेम", + "username_placeholder": "यूजरनेम दर्ज करें...", + "select_theme": "थीम चुनें", + "theme_placeholder": "प्रीसेट थीम चुनें", + "theme_mode": "थीम मोड", + "custom_bg": "कस्टम बैकग्राउंड", + "custom_accent": "कस्टम एक्सेंट", + "custom_text": "कस्टम टेक्स्ट", + "log_scaling": "लॉगरिदमिक हाइट स्केलिंग", + "speed": "एनिमेशन स्पीड", + "radius": "कॉर्नर रेडियस", + "badge_size": "बैज का आकार", + "sync_year": "सिंक वर्ष", + "clear_custom": "कस्टम रंग साफ़ करें", + "theme_presets": "थीम प्रीसेट", + "color_overrides": "कस्टम रंग ओवरराइड", + "font": "फ़ॉन्ट", + "custom_font_option": "कस्टम गूगल फ़ॉन्ट...", + "custom_font_placeholder": "उदा. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "एक्सपोर्ट स्निपेट", + "snippet_desc": "लाइव बैज कॉन्फ़िगरेशन को बदले बिना प्रारूप स्विच करें।", + "markdown": "मार्कडाउन", + "html": "एचटीएमएल", + "action": "GitHub Action", + "download_badge": "बैज डाउनलोड करें", + "downloading": "डाउनलोड हो रहा है...", + "download_not_available": "डाउनलोड उपलब्ध नहीं है", + "copy_workflow": "वर्कफ़्लो कॉपी करें", + "copy_format": "{{format}} कॉपी करें", + "copied": "कॉपी किया गया!", + "copy_aria_disabled": "निर्यात स्निपेट {{format}} कॉपी करने के लिए गिटहब यूजरनेम जोड़ें", + "copy_aria_enabled": "निर्यात स्निपेट {{format}} क्लिपबोर्ड पर कॉपी करें", + "footer_tip": "इसे अपनी गिटहब प्रोफ़ाइल के README.md में पेस्ट करें। बैज सर्वर-साइड रेंडर होता है, किसी स्क्रिप्ट की आवश्यकता नहीं है।", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "अपना खुद का डैशबोर्ड बनाएं", + "refresh_btn": "डेटा रीफ्रेश करें", + "refreshing": "रीफ्रेश हो रहा है...", + "refreshed_toast": "डैशबोर्ड सफलतापूर्वक रीफ्रेश किया गया", + "profile": { + "score": "डेवलपर स्कोर", + "repos": "रिपॉजिटरीज़", + "followers": "फ़ॉलोअर्स", + "following": "फ़ॉलोइंग", + "stars": "तारे (Stars)", + "joined": "शामिल हुए", + "pro_badge": "प्रो", + "pro": "PRO", + "share": "Share Your Pulse" + }, + "achievements": { + "title": "उपलब्धियां", + "see_all": "सभी उपलब्धियां देखें", + "show_less": "कम दिखाएं" + }, + "activity": { + "title": "गतिविधि परिदृश्य", + "year": "वर्ष", + "month": "महीना", + "week": "सप्ताह", + "commits": "कमिट्स", + "intensity": "कमिट तीव्रता", + "loc": "कोड की पंक्तियां", + "loc_desc": "समय के साथ संशोधित कोड की पंक्तियां", + "commits_desc": "समय के साथ कमिट की आवृत्ति", + "lines_modified": "{{count}} पंक्तियां संशोधित", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "शीर्ष भाषाएँ", + "primary": "प्राथमिक भाषा", + "no_data": "कोई भाषा डेटा नहीं मिला" + }, + "clock": { + "title": "कमिट क्लॉक", + "active_days": "साप्ताहिक गतिविधि चक्र", + "tooltip_no_commits": "इस सप्ताह के दिन के लिए कोई कमिट रिकॉर्ड नहीं किया गया", + "tooltip_peak": "इस चक्र में चरम कार्यदिवस", + "tooltip_activity": "साप्ताहिक गतिविधि बिंदु" + }, + "heatmap": { + "title": "योगदान हीटमैप", + "less": "कम", + "more": "अधिक", + "last_365": "पिछले 365 दिन", + "tooltip_single": "{{date}} को {{count}} योगदान", + "tooltip_plural": "{{date}} को {{count}} योगदान", + "no_activity": "कोई गतिविधि दर्ज नहीं", + "peak_activity": "अत्यधिक सक्रिय दिन", + "high_activity": "उच्च सक्रिय दिन", + "steady_contribution": "स्थिर योगदान का दिन", + "light_activity": "हल्की गतिविधि का दिन", + "no_active_streak": "कोई सक्रिय स्ट्रीक नहीं", + "active_streak": "{{streak}}-दिवसीय सक्रिय स्ट्रीक", + "empty": "दिखाने के लिए कोई हालिया गतिविधि नहीं है", + "code_activity": "कोड गतिविधि दर्ज की गई", + "no_code_changes": "कोई कोड परिवर्तन दर्ज नहीं" + }, + "insights": { + "title": "एआई अंतर्दृष्टि (AI Insights)" + }, + "stats": { + "current_streak": "वर्तमान स्ट्रीक", + "peak_streak": "अधिकतम स्ट्रीक", + "contributions": "कुल योगदान", + "days": "दिन", + "last_year": "पिछले वर्ष", + "utc_disclaimer": "स्ट्रीक की गणना UTC समय क्षेत्र में की गई है" + }, + "share": { + "title": "शेयर पल्स", + "close_aria": "शेयर विकल्प बंद करें", + "copy_link": "लिंक कॉपी करें", + "copy_link_desc": "अपने प्रोफ़ाइल यूआरएल को क्लिपबोर्ड पर कॉपी करें", + "link_copied": "लिंक कॉपी किया गया!", + "share_x": "X पर शेयर करें", + "share_x_desc": "दुनिया के साथ अपनी पल्स ट्वीट करें", + "share_linkedin": "लिंक्डइन पर शेयर करें", + "share_linkedin_desc": "अपने नेटवर्क पर अपनी कोडिंग गतिविधि पोस्ट करें", + "copy_markdown": "मार्कडाउन कॉपी करें", + "copy_markdown_desc": "README के लिए मार्कडाउन स्निपेट कॉपी करें", + "download_png": "PNG डाउनलोड करें", + "download_png_desc": "डैशबोर्ड का स्नैपशॉट सहेजें", + "download_svg": "SVG डाउनलोड करें", + "download_svg_desc": "मूल मोनोलिथ SVG डाउनलोड करें", + "download_json": "JSON डाउनलोड करें", + "download_json_desc": "स्ट्रीक और भाषा का कच्चा डेटा निर्यात करें", + "share_os": "OS शीट के माध्यम से शेयर करें", + "share_os_desc": "AirDrop, WhatsApp, संदेश और बहुत कुछ", + "share_os_fallback": "अधिक विकल्प", + "share_os_fallback_desc": "सिस्टम शेयर संवाद खोलें", + "share_reddit": "रेडิต", + "share_reddit_desc": "रेडिट पर शेयर करें", + "downloaded": "डाउनलोड किया गया!", + "svg_downloaded": "SVG डाउनलोड किया गया!", + "json_downloaded": "JSON डाउनलोड किया गया!", + "failed": "विफल — पुनः प्रयास करें", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "प्रिंट करने योग्य 3D STL मोनोलिथ डाउनलोड करें" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "योगदान का पूर्वानुमान", + "subtitle": "अपने कोडिंग वेग और प्रवृत्ति ढलानों के आधार पर भविष्य के विकास का पूर्वानुमान लगाएं।", + "weekly_velocity": "साप्ताहिक वेग", + "monthly_velocity": "मासिक वेग", + "projected_month": "अनुमानित महीना-अंत", + "projected_year": "अनुमानित वर्ष-अंत", + "consistency_rating": "स्थिरता रेटिंग", + "trend_direction": "प्रवृत्ति", + "commits_per_week": "{{count}} कमिट/सप्ताह", + "commits_per_month": "{{count}} कमिट/महीना", + "commits": "{{count}} कमिट", + "growth_rate": "विकास दर: {{rate}} कमिट/दिन²", + "consistency_level": { + "elite": "उत्कृष्ट (अति सुसंगत)", + "consistent": "उच्च (सुसंगत)", + "occasional": "मध्यम (प्रासंगिक)", + "sporadic": "कम (छिटपुट)", + "inactive": "निष्क्रिय" + }, + "trends": { + "strong_growth": "मजबूत विकास", + "moderate_growth": "मध्यम विकास", + "stable": "स्थिर लय", + "cooling": "धीमा हो रहा है", + "decline": "महत्वपूर्ण गिरावट" + }, + "no_activity": "भविष्यवाणी उत्पन्न करने के लिए कोई पिछला गतिविधि डेटा उपलब्ध नहीं है।" + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/ja.json b/scratch/ja.json new file mode 100644 index 000000000..a3f981304 --- /dev/null +++ b/scratch/ja.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "ホームに戻る", + "repo": "GitHub リポジトリ", + "contributors": "コントリビューター", + "theme_toggle": "テーマを切り替える", + "menu_open": "メニューを開く", + "menu_close": "メニューを閉じる", + "compare": "比較する", + "customization_studio": "カスタマイズスタジオ", + "generator": "ジェネレーター", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "エリートビルダーコミュニティ向けに設計されています。", + "contributors": "コントリビューター", + "documentation": "ドキュメント", + "creator": "開発者", + "copyright": "© {{year}} CommitPulse. All rights reserved.", + "navigation": "ナビゲーション", + "resources": "リソース", + "connect": "連携", + "made_with": "開発者のために ❤️ を込めて作成", + "home": "ホーム", + "compare": "比較", + "customization": "カスタマイズ", + "github_repo": "GitHub リポジトリ", + "github": "GitHub", + "creator_github": "GitHub の作者", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "ジェネレーター" + }, + "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", + "title": "あなたの{開発の軌跡}を\n立体的に表現する。", + "subtitle": "平面的なグリッドだけで満足していませんか? あなたのコーディングのリズムをプロフェッショナルな精度で可視化する、高精度な3Dアイソメトリックモノリスを作成しましょう。", + "watch_dashboard": "ダッシュボードを見る", + "discord_community": "Discordコミュニティに参加する", + "input_placeholder": "GitHubユーザー名を入力", + "clear_input": "入力をクリア", + "copy_link": "リンクをコピー", + "copied": "コピーされました", + "recent": "最近の検索:", + "clear": "クリア", + "preview_placeholder_title": "あなたのリズムを可視化する準備はできていますか?", + "preview_placeholder_desc": "上にGitHubのユーザー名を入力して、すぐにストリークバッジを生成します。", + "user_not_found": "GitHubユーザーが見つかりません", + "user_not_found_desc": "ユーザー名を確認して、もう一度お試しください。", + "preview_auto_theme": "ランダムテーマはページ読み込みごとに変化し、キャッシュを無効にします", + "preview_caching_tip": "プレビューは変更ごとに更新されます。ホストされたバッジはUTC深夜にキャッシュされます", + "preview_empty_tip": "ユーザー名を追加すると、ライブプレビューとエクスポートスニペットが有効になります", + "features": { + "sync_title": "リアルタイム同期", + "sync_desc": "GitHub GraphQL APIから直接取得。あなたの開発ストリークは、コードのプッシュに合わせて即座に更新されます。", + "theme_title": "テーマエンジン", + "theme_desc": "シンプルなURLパラメータ管理で、Neon、Dracula、またはカスタムHEXモードを切り替えることができます。", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." + }, + "generate_badge": "バッジを生成", + "verified_profile": "確認済みプロファイル", + "verifying": "検証中...", + "preview_monolith": "モノリスプレビュー", + "interactive_preview_title": "インタラクティブなモノリスプレビュー", + "interactive_preview_desc": "CommitPulseは、公開されているGitHubのコントリビューション履歴を、カスタマイズ可能な3D都市にコンパイルします。タワーが高いほど、その日のコミット数が多いことを示します。上にGitHubのユーザー名を入力して、今すぐストリークバッジを生成してください。", + "input_aria_label": "バッジを生成するにはGitHubユーザー名を入力してください", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "モノリスの準備ができました - 4つのステップで配置しましょう", + "markdown_copied": "Markdownがコピーされました", + "dismiss_aria": "Dismiss guide", + "step_1_title": "プロフィールリポジトリを開く", + "step_1_body": "github.com/あなたのユーザー名/あなたのユーザー名 - あなたの特別なプロフィールリポジトリに移動します。", + "step_2_title": "README.mdを編集する", + "step_2_body": "鉛筆アイコンをクリックして、GitHubの標準エディタでファイルを開きます。", + "step_3_title": "スニペットを貼り付ける", + "step_3_body": "モノリスを表示したい場所にカーソルを合わせ、貼り付けます(Ctrl+V / Cmd+V)。", + "step_4_title": "変更をコミットして公開する", + "step_4_body": "\"Commit changes\"をクリックしてプロフィールを確認します。これであなたの3Dストリークが公開されました。", + "copied_snippet_label": "コピーされたスニペット", + "color_tip": "ヒント: URLに ?accent=808080 を追加すると、モノリス의カラーパレットを変更できます。", + "watch_dashboard_btn": "ダッシュボードを確認する", + "customize_more_btn": "さらにカスタマイズしますか?" + }, + "customize_cta": { + "studio_badge": "カスタマイズスタジオ", + "title": "モノリスを微調整しますか?", + "desc": "ピクセル単位で調整可能 — アクセントカラーの変更、ダークやネオンテーマの適用、対数高さスケールの切り替えなどをリアルタイムでプレビューできます。", + "btn": "カスタマイズスタジオを開く" + }, + "customize": { + "back_to_home": "ホームに戻る", + "title": "モノリスを微調整。", + "desc": "以下の変更はすべてリアルタイムでプレビューに反映されます。完了したらエクスポートスニペットをコピーしてください。追加の手順は不要です。", + "live_preview": "ライブプレビュー", + "active_params": "有効なパラメータ", + "empty_preview_desc": "ユーザー名を追加すると、ここにライブプレビューが表示されます。", + "controls": { + "username": "GitHubユーザー名", + "username_placeholder": "ユーザー名を入力...", + "select_theme": "テーマを選択", + "theme_placeholder": "プリセットテーマを選択", + "theme_mode": "テーマモード", + "custom_bg": "カスタム背景", + "custom_accent": "カスタムアクセント", + "custom_text": "カスタムテキスト", + "log_scaling": "対数高さスケール", + "speed": "アニメーション速度", + "radius": "角の半径", + "badge_size": "バッジサイズ", + "sync_year": "同期対象年", + "clear_custom": "カスタムカラーをクリア", + "theme_presets": "テーマプリセット", + "color_overrides": "カスタムカラー上書き", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "エクスポートスニペット", + "snippet_desc": "バッジのライブ設定を変更することなく、エクスポート形式を切り替えることができます。", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", + "copy_format": "{{format}}をコピー", + "copied": "コピーされました!", + "copy_aria_disabled": "GitHubユーザー名を追加して{{format}}エクスポートスニペットをコピー", + "copy_aria_enabled": "{{format}}エクスポートスニペットをクリップボードにコピー", + "footer_tip": "これをGitHubプロフィールのREADME.mdに貼り付けます。バッジはサーバーサイドでレンダリングされるため、スクリプトは不要です。", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "自分のダッシュボードを生成", + "refresh_btn": "データを更新", + "refreshing": "更新中...", + "refreshed_toast": "ダッシュボードが正常に更新されました", + "profile": { + "score": "開発者スコア", + "repos": "リポジトリ数", + "followers": "フォロワー数", + "following": "フォロー数", + "stars": "スター獲得数", + "joined": "登録日", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" + }, + "achievements": { + "title": "実績", + "see_all": "実績をすべて表示", + "show_less": "一部非表示" + }, + "activity": { + "title": "活動状況", + "year": "年", + "month": "月", + "week": "週", + "commits": "コミット", + "intensity": "コミット強度", + "loc": "コード行数", + "loc_desc": "期間中に変更されたコード行数", + "commits_desc": "期間中のコミット颻度", + "lines_modified": "{{count}} 行変更", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "主な言語", + "primary": "主要言語", + "no_data": "言語データが見つかりません" + }, + "clock": { + "title": "コミットクロック", + "active_days": "週次アクティビティサイクル", + "tooltip_no_commits": "この曜日にはコミットが記録されていません", + "tooltip_peak": "このサイクルで最もアクティブな曜日", + "tooltip_activity": "週間の活動ポイント" + }, + "heatmap": { + "title": "コントリビューションヒートマップ", + "less": "控えめ", + "more": "多め", + "last_365": "過去365日", + "tooltip_single": "{{date}} に {{count}} 件のコントリビューション", + "tooltip_plural": "{{date}} に {{count}} 件のコントリビューション", + "no_activity": "アクティビティの記録なし", + "peak_activity": "アクティビティのピーク日", + "high_activity": "高いアクティビティの日", + "steady_contribution": "安定した貢献の日", + "light_activity": "軽いアクティビティの日", + "no_active_streak": "アクティブなストリークなし", + "active_streak": "{{streak}}日間のアクティブストリーク", + "empty": "表示する最近のアクティビティはありません", + "code_activity": "コード活動の記録あり", + "no_code_changes": "コード変更の記録なし" + }, + "insights": { + "title": "AIデータ分析" + }, + "stats": { + "current_streak": "現在のストリーク", + "peak_streak": "最大ストリーク", + "contributions": "総コントリビューション", + "days": "日", + "last_year": "昨年比", + "utc_disclaimer": "ストリークはUTCタイムゾーンで計算されます" + }, + "share": { + "title": "パルスを共有", + "close_aria": "共有オプションパネルを閉じる", + "copy_link": "リンクをコピー", + "copy_link_desc": "プロフィールURLをクリップボードにコピー", + "link_copied": "リンクがコピーされました!", + "share_x": "Xで共有", + "share_x_desc": "あなたの開発パルスをXにポスト", + "share_linkedin": "LinkedInで共有", + "share_linkedin_desc": "開発アクティビティをネットワークにポスト", + "copy_markdown": "Markdownをコピー", + "copy_markdown_desc": "README用のMarkdownスニペットをコピー", + "download_png": "PNGとしてダウンロード", + "download_png_desc": "ダッシュボードのスクリーンショットを保存", + "download_svg": "SVGをダウンロード", + "download_svg_desc": "生のモノリスSVGファイルをダウンロード", + "download_json": "JSONをダウンロード", + "download_json_desc": "ストリークと言語の生データをエクスポート", + "share_os": "OS標準シートで共有", + "share_os_desc": "AirDrop、WhatsApp、メッセージなど", + "share_os_fallback": "その他の共有オプション", + "share_os_fallback_desc": "システム標準の共有ダイアログを開く", + "share_reddit": "Reddit", + "share_reddit_desc": "Redditで共有", + "downloaded": "ダウンロードされました!", + "svg_downloaded": "SVGがダウンロードされました!", + "json_downloaded": "JSONがダウンロードされました!", + "failed": "失敗しました — 再試行してください", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "印刷可能な3D STLモノリスのダウンロード" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "貢献予測", + "subtitle": "コーディング速度とトレンドの傾きに基づいて将来의成長を予測します。", + "weekly_velocity": "週平均ベロシティ", + "monthly_velocity": "月平均ベロシティ", + "projected_month": "月末の予測値", + "projected_year": "年末の予測値", + "consistency_rating": "継続性評価", + "trend_direction": "トレンド", + "commits_per_week": "{{count}} コミット/週", + "commits_per_month": "{{count}} コミット/月", + "commits": "{{count}} コミット", + "growth_rate": "成長率: {{rate}} コミット/日²", + "consistency_level": { + "elite": "エリート (非常に安定)", + "consistent": "高い (安定)", + "occasional": "適度 (時々)", + "sporadic": "低い (散発的)", + "inactive": "非アクティブ" + }, + "trends": { + "strong_growth": "強い成長", + "moderate_growth": "緩やかな成長", + "stable": "安定したリズム", + "cooling": "減速中", + "decline": "大幅な低下" + }, + "no_activity": "予測を生成するための過去のアクティビティデータがありません。" + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/ko.json b/scratch/ko.json new file mode 100644 index 000000000..546ea7a24 --- /dev/null +++ b/scratch/ko.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "홈으로 이동", + "repo": "GitHub 저장소", + "contributors": "기여자", + "theme_toggle": "테마 전환", + "menu_open": "메뉴 열기", + "menu_close": "메뉴 닫기", + "compare": "비교하기", + "customization_studio": "커스터마이징 스튜디오", + "generator": "제너레이터", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "엘리트 빌더 커뮤니티를 위해 설계되었습니다.", + "contributors": "기여자", + "documentation": "문서", + "creator": "제작자", + "copyright": "© {{year}} CommitPulse. All rights reserved.", + "navigation": "네비게이션", + "resources": "리소스", + "connect": "연결", + "made_with": "개발자를 위해 ❤️로 제작됨", + "home": "홈", + "compare": "비교", + "customization": "커스터마이징", + "github_repo": "GitHub 저장소", + "github": "GitHub", + "creator_github": "GitHub의 제작자", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "제너레이터" + }, + "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", + "title": "{기여} 내역을\n더 돋보이게.", + "subtitle": "평면 그리드에 만족하지 마세요. 코딩 리듬을 전문적인 정밀도로 시각화해주는 고해상도 3D 등각 모놀리스를 생성하세요.", + "watch_dashboard": "대시보드 보기", + "discord_community": "Discord 커뮤니티 참여하기", + "input_placeholder": "GitHub 사용자 이름 입력", + "clear_input": "입력 지우기", + "copy_link": "링크 복사", + "copied": "복사됨", + "recent": "최근 검색:", + "clear": "지우기", + "preview_placeholder_title": "당신의 리듬을 시각화할 준비가 되셨나요?", + "preview_placeholder_desc": "스트릭 배지를 즉시 생성하려면 위에 GitHub 사용자 이름을 입력하세요.", + "user_not_found": "GitHub 사용자를 찾을 수 없습니다", + "user_not_found_desc": "사용자 이름을 확인하고 다시 시도해 주세요.", + "preview_auto_theme": "랜덤 테마는 페이지 로드할 때마다 변경되며 캐시를 비활성화합니다", + "preview_caching_tip": "미리보기는 변경 시마다 업데이트됩니다. 호스팅된 배지는 UTC 자정에 캐시됩니다", + "preview_empty_tip": "라이브 미리보기 및 내보내기 스니펫을 활성화하려면 사용자 이름을 추가하세요", + "features": { + "sync_title": "실시간 동기화", + "sync_desc": "GitHub GraphQL API에서 직접 가져옵니다. 코드를 푸시하는 것과 동시에 스트리크가 업데이트됩니다.", + "theme_title": "테마 엔진", + "theme_desc": "간단한 URL 매개변수 관리를 통해 Neon, Dracula 또는 사용자 정의 HEX 모드 간에 전환하세요.", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." + }, + "generate_badge": "빓지 생성", + "verified_profile": "인증된 프로필", + "verifying": "검증 줝...", + "preview_monolith": "모놀리스 미리보기", + "interactive_preview_title": "대화형 모놀리스 미리보기", + "interactive_preview_desc": "CommitPulse는 공개 GitHub 기여 내역음 맞춤설정 가능한 3D 도시로 컴파일합니다. 타워가 높을수록 그날 더 많은 커밋을 한 것입니다. 위에 GitHub 사용자 이름을 입력하여 지금 바로 스트리크 밷지리 생성하세요.", + "input_aria_label": "배지를 생성하려면 GitHub 사용자 이름을 입력하세요", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "모놀리스가 준비되었습니다 - 4단계로 배포하세요", + "markdown_copied": "Markdown이 복사되었습니다", + "dismiss_aria": "Dismiss guide", + "step_1_title": "프로필 저장소 열기", + "step_1_body": "github.com/사용자이름/사용자이름 - 특수 프로필 저장소로 이동합니다.", + "step_2_title": "README.md 수정", + "step_2_body": "연필 아이콘을 클릭하여 GitHub 내장 에디터로 파일을 엽니다.", + "step_3_title": "스니펫 붙여넣기", + "step_3_body": "모놀리스를 표시할 위치에 커서를 두고 붙여넣습니다 (Ctrl+V / Cmd+V).", + "step_4_title": "저장 및 배포", + "step_4_body": "\"Commit changes\"를 클릭하고 프로필을 방문하세요. 이제 3D 스트리크가 라이브로 표시됩니다.", + "copied_snippet_label": "복사된 스니펫", + "color_tip": "팁: URL에 ?accent=808080을 추가하여 모놀리스의 색상 팔레트를 변경하세요.", + "watch_dashboard_btn": "대시보드 확인하기", + "customize_more_btn": "더 맞춤 설정하고 싶으신가요?" + }, + "customize_cta": { + "studio_badge": "커스터마이징 스튜디오", + "title": "모놀리스를 미세 조정하고 싶으신가요?", + "desc": "모든 픽셀 조정 가능 — 강조 색상을 변경하고, 다크 또는 네온 테마를 적용하며, 로그 높이 배율을 켜고, 붙여넣기 전에 실시간으로 변경 사항을 확인하세요.", + "btn": "커스터마이징 스튜디오 열기" + }, + "customize": { + "back_to_home": "홈으로 돌아가기", + "title": "모놀리스 미세 조정.", + "desc": "아래의 모든 변경 사항은 실시간으로 미리보기에 업데이트됩니다. 작업이 끝나면 내보내기 스니펫을 복사하세요. 추가 단계가 필요 없습니다.", + "live_preview": "라이브 미리보기", + "active_params": "활성 매개변수", + "empty_preview_desc": "사용자 이름을 추가하면 배지 라이브 미리보기가 여기에 표시됩니다.", + "controls": { + "username": "GitHub 사용자 이름", + "username_placeholder": "사용자 이름 입력...", + "select_theme": "테마 선택", + "theme_placeholder": "프리셋 테마 선택", + "theme_mode": "테마 모드", + "custom_bg": "사용자 정의 배경", + "custom_accent": "사용자 정의 강조 색상", + "custom_text": "사용자 정의 텍스트", + "log_scaling": "로그 높이 배율", + "speed": "애니메이션 속도", + "radius": "모서리 둥글기", + "badge_size": "배지 크기", + "sync_year": "동기화 연도", + "clear_custom": "사용자 정의 색상 지우기", + "theme_presets": "테마 프리셋", + "color_overrides": "사용자 정의 색상 덮어쓰기", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "내보내기 스니펫", + "snippet_desc": "라이브 배지 설정을 변경하지 않고 내보내기 형식을 전환하세요.", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", + "copy_format": "{{format}} 복사", + "copied": "복사됨!", + "copy_aria_disabled": "내보내기 스니펫 {{format}}을 복사하려면 GitHub 사용자 이름을 추가하세요", + "copy_aria_enabled": "{{format}} 내보내기 스니펫을 클립보드에 복사", + "footer_tip": "이것을 GitHub 프로필 README.md 파일에 붙여넣으세요. 배지는 서버 측에서 렌더링되므로 스크립트가 필요하지 않습니다.", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "나만의 대시보드 만들기", + "refresh_btn": "데이터 새로고침", + "refreshing": "새로고치는 중...", + "refreshed_toast": "대시보드가 성공적으로 새로고침되었습니다", + "profile": { + "score": "개발자 점수", + "repos": "저장소", + "followers": "팔로워", + "following": "팔로잉", + "stars": "스타 수", + "joined": "가입일", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" + }, + "achievements": { + "title": "달성 업적", + "see_all": "모든 업적 보기", + "show_less": "일부만 보기" + }, + "activity": { + "title": "활동 현황", + "year": "연도", + "month": "월", + "week": "주", + "commits": "커밋", + "intensity": "커밋 강도", + "loc": "코드 라인 수", + "loc_desc": "시간 경과에 따른 수정된 코드 라인 수", + "commits_desc": "시간 경과에 따른 커밋 빈도", + "lines_modified": "{{count}} 라인 수정됨", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "주요 언어", + "primary": "주요 언어", + "no_data": "언어 데이터가 없습니다" + }, + "clock": { + "title": "커밋 시계", + "active_days": "주간 활동 주기", + "tooltip_no_commits": "이 요일에 기록된 커밋이 없습니다", + "tooltip_peak": "이 주기의 가장 활발한 요일", + "tooltip_activity": "주간 활동 지점" + }, + "heatmap": { + "title": "기여 히트맵", + "less": "적음", + "more": "많음", + "last_365": "지난 365일", + "tooltip_single": "{{date}}에 {{count}}회 기여", + "tooltip_plural": "{{date}}에 {{count}}회 기여", + "no_activity": "기록된 활동 없음", + "peak_activity": "최대 활동일", + "high_activity": "높은 활동일", + "steady_contribution": "꾸준한 기여일", + "light_activity": "가벼운 활동일", + "no_active_streak": "활성 스트릭 없음", + "active_streak": "{{streak}}일 연속 활성 스트릭", + "empty": "표시할 최근 활동이 없습니다", + "code_activity": "코드 활동 기록됬", + "no_code_changes": "코드 변경 기록 없음" + }, + "insights": { + "title": "AI 분석 통찰" + }, + "stats": { + "current_streak": "현재 스트리크", + "peak_streak": "최대 스트리크", + "contributions": "총 기여 수", + "days": "일", + "last_year": "지난해 대비", + "utc_disclaimer": "스트리크는 UTC 시간대를 기준으로 계산됩니다" + }, + "share": { + "title": "대시보드 공유", + "close_aria": "공유 옵션 패널 닫기", + "copy_link": "링크 복사", + "copy_link_desc": "프로필 URL을 클립보드에 복사", + "link_copied": "링크가 복사되었습니다!", + "share_x": "X에 공유", + "share_x_desc": "나만의 개발 파스를 X에 포스팅", + "share_linkedin": "LinkedIn에 공유", + "share_linkedin_desc": "개발 활동을 네트워크에 게시", + "copy_markdown": "Markdown 복사", + "copy_markdown_desc": "README용 Markdown 스니펫 복사", + "download_png": "PNG 다운로드", + "download_png_desc": "대시보드 캡처 이미지 저장", + "download_svg": "Download SVG", + "download_svg_desc": "원본 모놀리스 SVG 다운로드", + "download_json": "JSON 다운로드", + "download_json_desc": "스트리크 및 언어 원본 데이터 내보내기", + "share_os": "OS 표준 시트로 공유", + "share_os_desc": "AirDrop, WhatsApp, 메시지 등", + "share_os_fallback": "기타 공유 옵션", + "share_os_fallback_desc": "시스템 표준 공유 대화상자 열기", + "share_reddit": "Reddit", + "share_reddit_desc": "Reddit에 공유", + "downloaded": "다운로드되었습니다!", + "svg_downloaded": "SVG가 다운로드되었습니다!", + "json_downloaded": "JSON이 다운로드되었습니다!", + "failed": "실패 — 다시 시도하세요", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "인쇄 가능한 3D STL 모놀리스 다운로드" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "기여 예측", + "subtitle": "코딩 속도와 트렌드 기울기를 바탕으로 향후 성장을 예측합니다.", + "weekly_velocity": "주간 속도", + "monthly_velocity": "월간 속도", + "projected_month": "예상 월말 합계", + "projected_year": "예상 연말 합계", + "consistency_rating": "일관성 등급", + "trend_direction": "트렌드", + "commits_per_week": "주당 {{count}}회 커밋", + "commits_per_month": "월당 {{count}}회 커밋", + "commits": "{{count}}회 커밋", + "growth_rate": "성장률: {{rate}} 커밋/일²", + "consistency_level": { + "elite": "엘리트 (매우 일관됨)", + "consistent": "높음 (일관됨)", + "occasional": "보통 (가끔)", + "sporadic": "낮음 (산발적)", + "inactive": "비활성" + }, + "trends": { + "strong_growth": "강한 성장", + "moderate_growth": "완만한 성장", + "stable": "안정적인 리듬", + "cooling": "둔화됨", + "decline": "상당한 감소" + }, + "no_activity": "예측을 생성할 수 있는 이전 활동 데이터가 없습니다." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/pt.json b/scratch/pt.json new file mode 100644 index 000000000..688493847 --- /dev/null +++ b/scratch/pt.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "Ir para o início", + "repo": "Repositório GitHub", + "contributors": "Contribuidores", + "theme_toggle": "Alternar tema", + "menu_open": "Abrir menu", + "menu_close": "Fechar menu", + "compare": "Comparar", + "customization_studio": "Estúdio de Personalização", + "generator": "Gerador", + "burnout_radar": "Radar de Burnout" + }, + "footer": { + "tagline": "Projetado para a comunidade de desenvolvedores de elite.", + "contributors": "Contribuidores", + "documentation": "Documentação", + "creator": "Criador", + "copyright": "© {{year}} CommitPulse. Todos os direitos reservados.", + "navigation": "Navegação", + "resources": "Recursos", + "connect": "Conectar", + "made_with": "Feito com ❤️ para desenvolvedores", + "home": "Início", + "compare": "Comparar", + "customization": "Personalização", + "github_repo": "Repositório do GitHub", + "github": "GitHub", + "creator_github": "Criador no GitHub", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "Gerador" + }, + "landing": { + "empty_username_warning": "Por favor, insira um usuário do GitHub para copiar o link do seu emblema.", + "max_length_warning": "Limite de usuário do GitHub atingido (máximo de 39 caracteres)", + "title": "Eleve a História da\nSua {Contribution}.", + "subtitle": "Pare de se contentar com grades planas. Gere monólitos isométricos 3D de alta fidelidade que visualizam seu ritmo de codificação com precisão profissional.", + "watch_dashboard": "Ver Painel", + "discord_community": "Junte-se à comunidade principal no Discord", + "input_placeholder": "Inserir Usuário do GitHub", + "clear_input": "Limpar entrada", + "copy_link": "Copiar Link", + "copied": "Copiado", + "recent": "Recente:", + "clear": "Limpar", + "preview_placeholder_title": "Pronto para visualizar seu ritmo?", + "preview_placeholder_desc": "Insira um nome de usuário do GitHub acima para gerar instantaneamente seu emblema de sequência.", + "user_not_found": "Usuário do GitHub não encontrado", + "user_not_found_desc": "Por favor, verifique o nome de usuário e tente novamente.", + "preview_auto_theme": "O tema aleatório muda a cada carregamento de página e desativa o cache", + "preview_caching_tip": "Pré-visualize as atualizações a cada mudança. O emblema hospedado é armazenado em cache à meia-noite UTC", + "preview_empty_tip": "Adicione um nome de usuário para habilitar a pré-visualização ao vivo e exportar snippets", + "features": { + "sync_title": "Sincronização em Tempo Real", + "sync_desc": "Puxado diretamente da API GraphQL do GitHub. Sua sequência é atualizada tão rápido quanto seus pushes de código.", + "theme_title": "Motor de Temas", + "theme_desc": "Alterne entre os modos Neon, Dracula ou HEX personalizados via gerenciamento simples de URL.", + "isometric_title": "Matemática Isométrica", + "isometric_desc": "Fórmulas sofisticadas de projeção 3D transformam dados 2D em arquitetura digital." + }, + "generate_badge": "Gerar Emblema", + "verified_profile": "Perfil Verificado", + "verifying": "Verificando...", + "preview_monolith": "PRÉ-VISUALIZAR MONÓLITO", + "interactive_preview_title": "Pré-visualização Interativa do Monólito", + "interactive_preview_desc": "O CommitPulse compila seu histórico público de contribuições do GitHub em uma cidade 3D personalizável. Quanto mais altas as torres, mais você codou naquele dia. Insira um nome de usuário do GitHub acima para gerar instantaneamente seu emblema de sequência.", + "input_aria_label": "Insira o nome de usuário do GitHub para gerar o emblema", + "unable_to_load_stats": "Não foi possível carregar as estatísticas" + }, + "success_guide": { + "title": "Seu Monólito está Pronto - Implante-o em 4 Passos", + "markdown_copied": "Markdown Copiado", + "dismiss_aria": "Ignorar guia", + "step_1_title": "Abra Seu Repositório de Perfil", + "step_1_body": "Navegue até github.com/SEU_USUARIO/SEU_USUARIO - seu repositório especial de perfil.", + "step_2_title": "Edite README.md", + "step_2_body": "Clique no ícone de lápis para abrir o arquivo no editor integrado do GitHub.", + "step_3_title": "Cole o Snippet", + "step_3_body": "Coloque o cursor onde deseja que o monólito apareça e cole (Ctrl+V / Cmd+V).", + "step_4_title": "Salve e Lance", + "step_4_body": "Clique em \"Commit changes\" e visite seu perfil. Sua sequência 3D agora está ativa.", + "copied_snippet_label": "Seu snippet copiado", + "color_tip": "Dica: Adicione ?accent=808080 à URL para alterar a paleta de cores do seu monólito.", + "watch_dashboard_btn": "Assista ao Seu Painel", + "customize_more_btn": "Quer personalizar mais?" + }, + "customize_cta": { + "studio_badge": "Estúdio de Personalização", + "title": "Quer ajustar seu monólito?", + "desc": "Ajuste cada pixel — troque as cores de destaque, experimente um tema escuro ou neon, ative a escala logarítmica de altura e visualize as mudanças ao vivo antes de colar uma única linha.", + "btn": "Abrir Estúdio de Personalização" + }, + "customize": { + "back_to_home": "Voltar ao Início", + "title": "Ajuste o seu monólito.", + "desc": "Cada alteração abaixo atualiza a pré-visualização em tempo real. Copie o snippet de exportação quando terminar. Nenhum passo extra necessário.", + "live_preview": "Pré-visualização ao Vivo", + "active_params": "Parâmetros Ativos", + "empty_preview_desc": "A pré-visualização do emblema ao vivo aparecerá aqui assim que um nome de usuário for adicionado.", + "controls": { + "username": "Usuário do GitHub", + "username_placeholder": "Inserir usuário...", + "select_theme": "Selecionar Tema", + "theme_placeholder": "Selecionar Tema Predefinido", + "theme_mode": "Modo de Tema", + "custom_bg": "Fundo Personalizado", + "custom_accent": "Destaque Personalizado", + "custom_text": "Texto Personalizado", + "log_scaling": "Escala de Altura Logarítmica", + "speed": "Velocidade da Animação", + "radius": "Raio da Borda", + "badge_size": "Tamanho do Emblema", + "sync_year": "Sincronizar Ano", + "clear_custom": "Limpar Cores Personalizadas", + "theme_presets": "Predefinição de Tema", + "color_overrides": "Substituição de Cores Personalizadas", + "font": "Fonte", + "custom_font_option": "Fonte Personalizada do Google...", + "custom_font_placeholder": "ex: Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "Snippet de Exportação", + "snippet_desc": "Alterne os formatos sem alterar a configuração ao vivo do emblema.", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Baixar Emblema", + "downloading": "Baixando...", + "download_not_available": "Download Não Disponível", + "copy_workflow": "Copiar workflow", + "copy_format": "Copiar {{format}}", + "copied": "Copiado!", + "copy_aria_disabled": "Adicione um nome de usuário do GitHub para habilitar a cópia do snippet de exportação {{format}}", + "copy_aria_enabled": "Copiar snippet de exportação {{format}} para a área de transferência", + "footer_tip": "Cole isso no README.md do perfil do GitHub. O emblema é renderizado no lado do servidor, sem necessidade de script.", + "tsx": "React TSX", + "download_svg": "Baixar SVG", + "download_png": "Baixar PNG" + } + }, + "dashboard": { + "generate_btn": "Gere Seu Próprio Painel", + "refresh_btn": "Atualizar Dados", + "refreshing": "Atualizando...", + "refreshed_toast": "Painel atualizado com sucesso", + "profile": { + "score": "Pontuação do Desenvolvedor", + "repos": "Repositórios", + "followers": "Seguidores", + "following": "Seguindo", + "stars": "Estrelas", + "joined": "Entrou", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Compartilhar Seu Ritmo" + }, + "achievements": { + "title": "Conquistas", + "see_all": "Ver Todas as Conquistas", + "show_less": "Mostrar Menos" + }, + "activity": { + "title": "Cenário de Atividade", + "year": "Ano", + "month": "Mês", + "week": "Semana", + "commits": "Commits", + "intensity": "Intensidade de Commits", + "loc": "Linhas de Código", + "loc_desc": "Linhas de código modificadas ao longo do tempo", + "commits_desc": "Frequência de commits ao longo do tempo", + "lines_modified": "{{count}} linhas modificadas", + "aria_range": "de {{start}} até {{end}}", + "aria_single": "em {{date}}" + }, + "languages": { + "title": "Principais Linguagens", + "primary": "Linguagem Principal", + "no_data": "Nenhum dado de linguagem encontrado" + }, + "clock": { + "title": "Relógio de Commits", + "active_days": "Ciclo de atividade semanal", + "tooltip_no_commits": "Nenhum commit registrado para este dia da semana", + "tooltip_peak": "Dia de pico neste ciclo", + "tooltip_activity": "Ponto de atividade semanal" + }, + "heatmap": { + "title": "Mapa de Calor de Contribuições", + "less": "Menos", + "more": "Mais", + "last_365": "Últimos 365 dias", + "tooltip_single": "{{count}} contribuição em {{date}}", + "tooltip_plural": "{{count}} contribuições em {{date}}", + "no_activity": "Nenhuma atividade registrada", + "peak_activity": "Dia de pico de atividade", + "high_activity": "Dia de alta atividade", + "steady_contribution": "Dia de contribuição constante", + "light_activity": "Dia de atividade leve", + "no_active_streak": "Sem sequência ativa", + "active_streak": "Sequência ativa de {{streak}} dias", + "empty": "Nenhuma atividade recente para exibir", + "code_activity": "Atividade de código registrada", + "no_code_changes": "Nenhuma mudança de código registrada" + }, + "insights": { + "title": "Insights de IA" + }, + "stats": { + "current_streak": "Sequência Atual", + "peak_streak": "Sequência Máxima", + "contributions": "Contribuições", + "days": "Dias", + "last_year": "Último Ano", + "utc_disclaimer": "Sequência calculada no fuso horário UTC" + }, + "share": { + "title": "Compartilhar Ritmo", + "close_aria": "Fechar painel de compartilhamento", + "copy_link": "Copiar Link", + "copy_link_desc": "Copiar URL do seu perfil para a área de transferência", + "link_copied": "Link copiado", + "share_x": "Compartilhar no X", + "share_x_desc": "Tweete seu ritmo para o mundo", + "share_linkedin": "LinkedIn", + "share_linkedin_desc": "Publique sua atividade de dev para sua rede", + "copy_markdown": "Copiar Markdown do README", + "copy_markdown_desc": "Copiar snippet de markdown para o seu README", + "download_png": "Baixar Imagem PNG", + "download_png_desc": "Salvar uma imagem do seu painel", + "download_svg": "Baixar Monólito Vetorial em SVG", + "download_svg_desc": "Baixar o monólito original em SVG", + "download_json": "Exportar Dados Estruturados em JSON", + "download_json_desc": "Exportar os dados brutos de sequência e linguagem", + "share_os": "Compartilhar Sistema", + "share_os_desc": "AirDrop, WhatsApp, Mensagens e mais", + "share_os_fallback": "Mais Opções", + "share_os_fallback_desc": "Abrir o diálogo de compartilhamento do sistema", + "share_reddit": "Reddit", + "share_reddit_desc": "Compartilhar no Reddit", + "downloaded": "Ativo Salvo!", + "svg_downloaded": "SVG Baixado!", + "json_downloaded": "JSON Baixado!", + "failed": "Falhou — tente novamente", + "social_channels": "Canais Sociais", + "export_options": "Opções de Exportação", + "github_wrapped": "Retrospectiva GitHub", + "download_webp": "Baixar WebP Otimizado", + "download_stl": "Baixar STL 3D para Impressão" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "Previsão de Contribuição", + "subtitle": "Preveja o crescimento futuro com base na sua velocidade de codificação e inclinações de tendência.", + "weekly_velocity": "Velocidade Semanal", + "monthly_velocity": "Velocidade Mensual", + "projected_month": "Final de Mês Projetado", + "projected_year": "Final de Ano Projetado", + "consistency_rating": "Classificação de Consistência", + "trend_direction": "Tendência", + "commits_per_week": "{{count}} commits/sem", + "commits_per_month": "{{count}} commits/mês", + "commits": "{{count}} commits", + "growth_rate": "Taxa de Crescimento: {{rate}} commits/dia²", + "consistency_level": { + "elite": "Elite (Muito Consistente)", + "consistent": "Alta (Consistente)", + "occasional": "Moderada (Ocasional)", + "sporadic": "Baixa (Esporádica)", + "inactive": "Inativo" + }, + "trends": { + "strong_growth": "Crescimento Forte", + "moderate_growth": "Crescimento Moderado", + "stable": "Ritmo Estável", + "cooling": "Esfriamento", + "decline": "Declínio Significativo" + }, + "no_activity": "Não há dados de atividade passada disponíveis para gerar previsões." + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +} diff --git a/scratch/zh.json b/scratch/zh.json new file mode 100644 index 000000000..3031ca987 --- /dev/null +++ b/scratch/zh.json @@ -0,0 +1,377 @@ +{ + "navbar": { + "home": "返回首页", + "repo": "GitHub 仓库", + "contributors": "贡献者", + "theme_toggle": "切换主题", + "menu_open": "打开菜单", + "menu_close": "关闭菜单", + "compare": "对比", + "customization_studio": "自定义工作室", + "generator": "生成器", + "burnout_radar": "Burnout Radar" + }, + "footer": { + "tagline": "专为精英开发者社区设计。", + "contributors": "贡献者", + "documentation": "开发文档", + "creator": "创作者", + "copyright": "© {{year}} CommitPulse. 保留所有权利。", + "navigation": "导航", + "resources": "资源", + "connect": "联系我们", + "made_with": "为开发者倾心 ❤️ 打造", + "home": "首页", + "compare": "对比", + "customization": "自定义", + "github_repo": "GitHub 仓库", + "github": "GitHub", + "creator_github": "GitHub 上的创建者", + "discord": "Discord", + "twitter": "Twitter", + "linkedin": "LinkedIn", + "generator": "生成器" + }, + "landing": { + "empty_username_warning": "Please enter a GitHub username to copy your badge link.", + "max_length_warning": "GitHub username limit reached (39 characters maximum)", + "title": "提升您的\n{贡献}故事。", + "subtitle": "别再满足于扁平的网格。生成高保真的 3D 等距柱状图,以专业的精度可视化您的编码节奏。", + "watch_dashboard": "查看仪表盘", + "discord_community": "加入 Discord 社区", + "input_placeholder": "输入 GitHub 用户名", + "clear_input": "清空输入", + "copy_link": "复制链接", + "copied": "已复制", + "recent": "最近搜索:", + "clear": "清除", + "preview_placeholder_title": "准备好可视化你的节奏了吗?", + "preview_placeholder_desc": "在上方输入 GitHub 用户名以立即生成你的连击徽章。", + "user_not_found": "未找到 GitHub 用户", + "user_not_found_desc": "请检查用户名并重试。", + "preview_auto_theme": "随机主题在每次加载页面时都会更改,且禁用缓存", + "preview_caching_tip": "预览在每次更改时都会更新。托管的徽章在 UTC 午夜被缓存", + "preview_empty_tip": "添加用户名以启用实时预览和导出代码片段", + "features": { + "sync_title": "实时同步", + "sync_desc": "直接从 GitHub GraphQL API 获取。您的连续提交天数与代码推送速度保持同步。", + "theme_title": "主题引擎", + "theme_desc": "通过简单的 URL 管理在 Neon、Dracula 或自定义十六进制 (HEX) 模式之间进行切换。", + "isometric_title": "Isometric Math", + "isometric_desc": "Sophisticated 3D projection formulas turn 2D data into digital architecture." + }, + "generate_badge": "生成徽章", + "verified_profile": "已验证的个人资料", + "verifying": "正在验证...", + "preview_monolith": "预览单体", + "interactive_preview_title": "交互式单体预览", + "interactive_preview_desc": "CommitPulse 将您的公开 GitHub 贡甮历史编译成一个可定制的 3D 城市。塔越高,代表您那天的提交越多。在上方输入 GitHub 用户名即可立即生成您皅连续提交徽章。", + "input_aria_label": "输入 GitHub 用户名以生成徽章", + "unable_to_load_stats": "Unable to load stats" + }, + "success_guide": { + "title": "您的柱状图已准备就绪 - 仅需 4 步即可部署", + "markdown_copied": "Markdown 已复制", + "dismiss_aria": "Dismiss guide", + "step_1_title": "打开您的个人主页仓库", + "step_1_body": "前往 github.com/您的用户名/您的用户名 - 您的专属个人资料仓库。", + "step_2_title": "编辑 README.md", + "step_2_body": "点击铅笔图标以在 GitHub 内置的编辑器中打开文件。", + "step_3_title": "粘贴代码片段", + "step_3_body": "将光标移至您希望柱状图出现的位置,然后粘贴 (Ctrl+V / Cmd+V)。", + "step_4_title": "保存并发布", + "step_4_body": "点击 \"Commit changes\" 并查看您的个人主页。您的 3D 柱状图现在已上线。", + "copied_snippet_label": "已复制的代码片段", + "color_tip": "提示:在 URL 中添加 ?accent=808080 以更改您的柱状图配色方案。", + "watch_dashboard_btn": "查看您的仪表盘", + "customize_more_btn": "想要更多自定义选项?" + }, + "customize_cta": { + "studio_badge": "自定义工作室", + "title": "想要微调您的柱状图吗?", + "desc": "精确调整每个像素 — 调整强调色、尝试暗色或霓虹主题、切换对数高度缩放,并在粘贴任何代码之前实时预览所有更改。", + "btn": "打开自定义工作室" + }, + "customize": { + "back_to_home": "返回首页", + "title": "微调您的柱状图。", + "desc": "下方的每项更改都会实时更新预览。完成后复制导出代码片段。无需额外操作。", + "live_preview": "实时预览", + "active_params": "当前参数", + "empty_preview_desc": "添加用户名后,实时徽章预览将在此处显示。", + "controls": { + "username": "GitHub 用户名", + "username_placeholder": "输入用户名...", + "select_theme": "选择主题", + "theme_placeholder": "选择预设主题", + "theme_mode": "主题模式", + "custom_bg": "自定义背景", + "custom_accent": "自定义强调色", + "custom_text": "自定义文本", + "log_scaling": "对数高度缩放", + "speed": "动画速度", + "radius": "圆角半径", + "badge_size": "徽章大小", + "sync_year": "同步年份", + "clear_custom": "清空自定义颜色", + "theme_presets": "主题预设", + "color_overrides": "自定义颜色覆盖", + "font": "Font", + "custom_font_option": "Custom Google Font...", + "custom_font_placeholder": "e.g. Orbitron, Space Mono, Inter" + }, + "export": { + "snippet_title": "导出片段", + "snippet_desc": "在不改变实时徽章配置的情况下切换导出格式。", + "markdown": "Markdown", + "html": "HTML", + "action": "GitHub Action", + "download_badge": "Download Badge", + "downloading": "Downloading...", + "download_not_available": "Download Not Available", + "copy_workflow": "Copy workflow", + "copy_format": "复制 {{format}}", + "copied": "已复制!", + "copy_aria_disabled": "添加 GitHub 用户名以复制 {{format}} 导出片段", + "copy_aria_enabled": "将 {{format}} 导出片段复制到剪贴板", + "footer_tip": "将此片段粘贴到您的 GitHub 个人主页 README.md 文件中。徽章由服务器端渲染,无需任何脚本。", + "tsx": "React TSX", + "download_svg": "Download SVG", + "download_png": "Download PNG" + } + }, + "dashboard": { + "generate_btn": "生成您自己的仪表盘", + "refresh_btn": "刷新数据", + "refreshing": "正在刷新...", + "refreshed_toast": "仪表盘刷新成功", + "profile": { + "score": "开发者评分", + "repos": "公共仓库", + "followers": "粉丝数量", + "following": "关注数量", + "stars": "收获星标", + "joined": "加入时间", + "pro_badge": "PRO", + "pro": "PRO", + "share": "Share Your Pulse" + }, + "achievements": { + "title": "个人成就", + "see_all": "查看所有成就", + "show_less": "显示部分" + }, + "activity": { + "title": "活动全景", + "year": "年", + "month": "月", + "week": "周", + "commits": "次提交", + "intensity": "提交强度", + "loc": "代码行数", + "loc_desc": "随时间变化修改的代码行数", + "commits_desc": "随时间变化皅提交频率", + "lines_modified": "已修改 {{count}} 行代码", + "aria_range": "from {{start}} to {{end}}", + "aria_single": "on {{date}}" + }, + "languages": { + "title": "热门语言", + "primary": "主要开发语言", + "no_data": "未找到语言数据" + }, + "clock": { + "title": "提交时钟", + "active_days": "每周活动周期", + "tooltip_no_commits": "该工作日无提交记录", + "tooltip_peak": "此周期中的最活跃工作日", + "tooltip_activity": "每周活动点" + }, + "heatmap": { + "title": "贡献热力图", + "less": "较少", + "more": "较多", + "last_365": "过去 365 天", + "tooltip_single": "在 {{date}} 进行了 {{count}} 次贡献", + "tooltip_plural": "在 {{date}} 进行了 {{count}} 次贡献", + "no_activity": "无活动记录", + "peak_activity": "巅峰活动日", + "high_activity": "高活动日", + "steady_contribution": "稳定贡献日", + "light_activity": "轻度活动日", + "no_active_streak": "无活跃连续记录", + "active_streak": "连续 {{streak}} 天活跃记录", + "empty": "没有最近的活动可以显示", + "code_activity": "有代码活动记录", + "no_code_changes": "无代码修改记录" + }, + "insights": { + "title": "AI 数据洞察" + }, + "stats": { + "current_streak": "当前连续天数", + "peak_streak": "历史最高连续", + "contributions": "总贡献次数", + "days": "天", + "last_year": "去年的提交", + "utc_disclaimer": "提交天数以 UTC 时区计算" + }, + "share": { + "title": "分享数据脉搏", + "close_aria": "关闭分享面板", + "copy_link": "复制链接", + "copy_link_desc": "将您的个人仪表盘 URL 复制到剪贴板", + "link_copied": "链接已复制!", + "share_x": "分享到 X", + "share_x_desc": "将您的贡献脉搏推送到 X", + "share_linkedin": "分享到 LinkedIn", + "share_linkedin_desc": "向您的网络发布您的开发活动", + "copy_markdown": "复制 Markdown", + "copy_markdown_desc": "复制适用于您 README 的 Markdown 片段", + "download_png": "下载为 PNG", + "download_png_desc": "保存您仪表盘 of 的截图", + "download_svg": "下载 SVG", + "download_svg_desc": "下载原始等距柱状图 SVG 图像", + "download_json": "下载 JSON", + "download_json_desc": "导出连续天数和语言的原始 JSON 数据", + "share_os": "通过系统分享", + "share_os_desc": "AirDrop, 微信, 短信等更多渠道", + "share_os_fallback": "更多选项", + "share_os_fallback_desc": "打开系统原生分享对话框", + "share_reddit": "Reddit", + "share_reddit_desc": "分享到 Reddit", + "downloaded": "已下载!", + "svg_downloaded": "SVG 已下载!", + "json_downloaded": "JSON 已下载!", + "failed": "操作失败 — 请重试", + "social_channels": "Social Channels", + "export_options": "Export Options", + "github_wrapped": "GitHub Wrapped", + "download_webp": "Download Optimized WebP", + "download_stl": "下载可打印的 3D STL 整体结构" + }, + "impact": { + "title": "Repository Impact Analyzer", + "ranking": "Repository Ranking", + "languages": "Language Contribution", + "growth": "Growth Metrics", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "score": "Impact Score", + "age": "Repository Age", + "avg_stars": "Avg. Stars / Month", + "avg_forks": "Avg. Forks / Month", + "months": "months", + "years": "years", + "top_repos": "Top 5 Repositories", + "contribution_commits": "commits", + "no_data": "No repository data available." + }, + "forecast": { + "title": "贡献预测", + "subtitle": "基于您的代码提交速度和趋势斜率预测未来的增长趋势。", + "weekly_velocity": "周平均速度", + "monthly_velocity": "月平均速度", + "projected_month": "预测月末总计", + "projected_year": "预测年末总计", + "consistency_rating": "稳定性评级", + "trend_direction": "趋势", + "commits_per_week": "{{count}} 次提交/周", + "commits_per_month": "{{count}} 次提交/月", + "commits": "{{count}} 次提交", + "growth_rate": "增长率: {{rate}} 次提交/天²", + "consistency_level": { + "elite": "极佳 (非常稳定)", + "consistent": "高 (稳定)", + "occasional": "中 (偶尔)", + "sporadic": "低 (零星)", + "inactive": "未活跃" + }, + "trends": { + "strong_growth": "强劲增长", + "moderate_growth": "温和增长", + "stable": "稳定节奏", + "cooling": "有所放缓", + "decline": "显著下降" + }, + "no_activity": "暂无过去的历史活动数据以生成预测。" + }, + "replay": { + "title": "Contribution Replay Timeline", + "subtitle": "Animate your coding journey and watch commits accumulate month-by-month", + "play": "Play Replay", + "pause": "Pause Replay", + "reset": "Reset Replay", + "speed": "Playback Speed", + "speed_value": "{{speed}}x", + "accumulated": "Accumulated Commits", + "monthly_total": "Monthly Commits", + "peak_activity": "Peak Activity", + "peak_commits": "{{count}} commits", + "peak_date": "on {{date}}", + "no_activity": "No contribution activity data available for replay.", + "aria_scrub": "Scrub timeline months", + "aria_speed": "Select playback speed multiplier", + "aria_month": "Jump to {{month}}", + "active_month": "Active Month", + "intensity_breakdown": "Intensity Breakdown", + "intensity_low": "Low Intensity Days", + "intensity_medium": "Medium Intensity Days", + "intensity_high": "High Intensity Days" + }, + "compare": { + "title": "Profile Comparison Analytics", + "subtitle": "Compare developer activity, language breakdown, achievements, and repository impact head-to-head.", + "vs": "VS", + "developer_score": "Developer Score", + "contributions": "Contributions", + "streak": "Streak", + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + }, + "contribution_comparison": { + "title": "Contribution Comparison", + "subtitle": "Month-by-month developer activity level", + "user_total": "{{name}}'s Total: {{count}} contributions", + "no_data": "No activity data available" + }, + "language_comparison": { + "title": "Language Comparison", + "subtitle": "Side-by-side programming languages breakdown", + "no_data": "No language data found" + }, + "achievement_comparison": { + "title": "Achievement Comparison", + "subtitle": "Key achievements unlocked head-to-head", + "unlocked_by": "Unlocked by", + "both": "Both", + "neither": "Neither", + "unlocked": "Unlocked", + "locked": "Locked", + "no_data": "No achievements found" + }, + "repository_comparison": { + "title": "Repository Comparison", + "subtitle": "Head-to-head comparison of top 3 repositories", + "commits": "Commits", + "stars": "Stars", + "forks": "Forks", + "impact_score": "Impact Score", + "no_data": "No repository data found", + "rank": "Rank" + } + } + } +}