diff --git a/src/components/performance/CoreWebVitals.tsx b/src/components/performance/CoreWebVitals.tsx index 3d2692e8..8058b77e 100644 --- a/src/components/performance/CoreWebVitals.tsx +++ b/src/components/performance/CoreWebVitals.tsx @@ -1,8 +1,8 @@ -'use client'; - import React from 'react'; import type { PerformanceMetric } from '@/utils/performanceUtils'; import { formatMetricValue } from '@/utils/performanceUtils'; +import { useInternationalization } from '@/hooks/useInternationalization'; +import { translateWithFallback } from '@/components/dashboard/dashboardI18n'; export interface CoreWebVitalsProps { metrics: Record; @@ -15,16 +15,37 @@ const RATING_STYLES: Record, string> = poor: 'border-red-500/40 bg-red-500/10 text-red-900 dark:text-red-100', }; +const RATING_KEYS: Record, string> = { + good: 'performance.dashboard.vitals.ratings.good', + 'needs-improvement': 'performance.dashboard.vitals.ratings.needsImprovement', + poor: 'performance.dashboard.vitals.ratings.poor', +}; + +const RATING_FALLBACKS: Record, string> = { + good: 'Good', + 'needs-improvement': 'Needs Improvement', + poor: 'Poor', +}; + const ORDER = ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] as const; /** * Read-only grid of latest Core Web Vitals with rating-colored cards. */ export const CoreWebVitals: React.FC = ({ metrics, className = '' }) => { + const { t } = useInternationalization(); + return ( -
+

- Core Web Vitals + {translateWithFallback(t, 'performance.dashboard.vitals.heading', 'Core Web Vitals')}

    {ORDER.map((name) => { @@ -40,10 +61,12 @@ export const CoreWebVitals: React.FC = ({ metrics, className {m?.rating ? (
    - {m.rating.replace(/-/g, ' ')} + {translateWithFallback(t, RATING_KEYS[m.rating], RATING_FALLBACKS[m.rating])}
    ) : ( -
    Waiting…
    +
    + {translateWithFallback(t, 'performance.dashboard.vitals.waiting', 'Waiting…')} +
    )} ); diff --git a/src/components/performance/OptimizationSuggestions.tsx b/src/components/performance/OptimizationSuggestions.tsx index 2bf606c9..75b9768d 100644 --- a/src/components/performance/OptimizationSuggestions.tsx +++ b/src/components/performance/OptimizationSuggestions.tsx @@ -1,8 +1,8 @@ -'use client'; - import React from 'react'; import { Lightbulb } from 'lucide-react'; import type { OptimizationSuggestion } from '@/utils/performanceUtils'; +import { useInternationalization } from '@/hooks/useInternationalization'; +import { translateWithFallback } from '@/components/dashboard/dashboardI18n'; export interface OptimizationSuggestionsProps { suggestions: OptimizationSuggestion[]; @@ -18,28 +18,46 @@ export const OptimizationSuggestions: React.FC = ( suggestions, className = '', }) => { + const { t } = useInternationalization(); const sorted = [...suggestions].sort((a, b) => IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact]); + const sectionAriaLabel = translateWithFallback( + t, + 'performance.telemetry.suggestions.heading', + 'Optimization suggestions', + ); + if (sorted.length === 0) { return ( -
    +

    - Suggestions + {translateWithFallback( + t, + 'performance.telemetry.suggestions.headingFallback', + 'Suggestions', + )}

    - No issues detected from current Core Web Vitals. Keep monitoring as users interact with - the app. + {translateWithFallback( + t, + 'performance.telemetry.suggestions.empty', + 'No issues detected from current Core Web Vitals. Keep monitoring as users interact with the app.', + )}

    ); } return ( -
    +

    - Optimization suggestions + {translateWithFallback( + t, + 'performance.telemetry.suggestions.heading', + 'Optimization suggestions', + )}

      {sorted.map((s) => ( @@ -48,9 +66,26 @@ export const OptimizationSuggestions: React.FC = ( className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950/60 p-3" >
      - {s.title} + + {translateWithFallback( + t, + `performance.telemetry.suggestions.${s.id}.title`, + s.title, + )} + - {s.impact} impact + {translateWithFallback( + t, + 'performance.telemetry.suggestions.impact', + `${s.impact} impact`, + { + impact: translateWithFallback( + t, + `performance.telemetry.suggestions.impactLevels.${s.impact}`, + s.impact, + ), + }, + )} {s.metric ? ( @@ -58,7 +93,13 @@ export const OptimizationSuggestions: React.FC = ( ) : null}
      -

      {s.detail}

      +

      + {translateWithFallback( + t, + `performance.telemetry.suggestions.${s.id}.detail`, + s.detail, + )} +

      ))}
    diff --git a/src/components/performance/PerformanceDashboard.tsx b/src/components/performance/PerformanceDashboard.tsx index dceb9e52..3c5af0e6 100644 --- a/src/components/performance/PerformanceDashboard.tsx +++ b/src/components/performance/PerformanceDashboard.tsx @@ -1,5 +1,3 @@ -'use client'; - import React, { useMemo } from 'react'; import Link from 'next/link'; import { Toaster } from 'react-hot-toast'; @@ -28,6 +26,8 @@ import { clearTrendHistory, type PerformanceTrendPoint } from '@/utils/performan import { CoreWebVitals } from './CoreWebVitals'; import { OptimizationSuggestions } from './OptimizationSuggestions'; import { Breadcrumbs, type BreadcrumbItem } from '@/components/ui/Breadcrumbs'; +import { useInternationalization } from '@/hooks/useInternationalization'; +import { translateWithFallback } from '@/components/dashboard/dashboardI18n'; const VITAL_NAMES = ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] as const; @@ -50,6 +50,7 @@ function formatTick(name: string, value: number): string { * Full-screen performance monitoring: Core Web Vitals, alerts, suggestions, and trend charts. */ export const PerformanceDashboard: React.FC = () => { + const { t } = useInternationalization(); const { metrics, alerts, suggestions, trend, clearAlerts, refreshTrendFromStorage } = usePerformanceMonitoring(); @@ -78,18 +79,33 @@ export const PerformanceDashboard: React.FC = () => {

    - Performance dashboard + {translateWithFallback(t, 'performance.dashboard.title', 'Performance dashboard')}

    - Core Web Vitals, recent alerts, and session trend samples (stored in sessionStorage). + {translateWithFallback( + t, + 'performance.dashboard.subtitle', + 'Core Web Vitals, recent alerts, and session trend samples (stored in sessionStorage).', + )}

    @@ -109,11 +125,21 @@ export const PerformanceDashboard: React.FC = () => {

    - Analytics Status + {translateWithFallback( + t, + 'performance.dashboard.statusPanel.heading', + 'Analytics Status', + )}

    - Status + + {translateWithFallback( + t, + 'performance.dashboard.statusPanel.statusLabel', + 'Status', + )} + { }`} > {isAnalyticsEnabled ? : null} - {isAnalyticsEnabled ? 'Active' : 'Disabled (Dev)'} + {isAnalyticsEnabled + ? translateWithFallback(t, 'performance.dashboard.statusPanel.active', 'Active') + : translateWithFallback( + t, + 'performance.dashboard.statusPanel.disabled', + 'Disabled (Dev)', + )}
    - Endpoint + + {translateWithFallback( + t, + 'performance.dashboard.statusPanel.endpoint', + 'Endpoint', + )} + /api/performance/vitals @@ -134,7 +172,11 @@ export const PerformanceDashboard: React.FC = () => {

    - Simulated Global Average (7d) + {translateWithFallback( + t, + 'performance.dashboard.statusPanel.simulated', + 'Simulated Global Average (7d)', + )}

    {[ @@ -159,7 +201,7 @@ export const PerformanceDashboard: React.FC = () => {

    - Alerts + {translateWithFallback(t, 'performance.dashboard.alertsPanel.heading', 'Alerts')}

    {alerts.length > 0 ? ( ) : null}
    {alerts.length === 0 ? (

    - No degradation alerts in this session. + {translateWithFallback( + t, + 'performance.dashboard.alertsPanel.empty', + 'No degradation alerts in this session.', + )}

    ) : (
      - {alerts.map((a) => ( -
    • - - {new Date(a.at).toLocaleTimeString()} - - {a.message} -
    • - ))} + {alerts.map((a) => { + const formattedValue = a.message.match(/\(([^)]+)\)/)?.[1] || ''; + const translatedMessage = translateWithFallback( + t, + `performance.telemetry.alerts.${a.severity === 'critical' ? 'poor' : 'warning'}`, + a.message, + { + name: a.metricName, + value: formattedValue, + }, + ); + return ( +
    • + + {new Date(a.at).toLocaleTimeString()} + + {translatedMessage} +
    • + ); + })}
    )}
    @@ -201,7 +263,11 @@ export const PerformanceDashboard: React.FC = () => {
    {VITAL_NAMES.map((name) => { @@ -216,10 +282,23 @@ export const PerformanceDashboard: React.FC = () => { {data.length < 2 ? (

    - Not enough samples yet. Interact with the app or reload to collect points. + {translateWithFallback( + t, + 'performance.dashboard.chartPlaceholder', + 'Not enough samples yet. Interact with the app or reload to collect points.', + )}

    ) : ( -
    +
    { dataKey="i" tick={{ fontSize: 10 }} label={{ - value: 'Sample #', + value: translateWithFallback( + t, + 'performance.dashboard.xAxisLabel', + 'Sample #', + ), position: 'insideBottom', offset: -4, fontSize: 10, diff --git a/src/components/performance/PerformanceMonitor.tsx b/src/components/performance/PerformanceMonitor.tsx index 984ef1d5..12f565c6 100644 --- a/src/components/performance/PerformanceMonitor.tsx +++ b/src/components/performance/PerformanceMonitor.tsx @@ -5,6 +5,8 @@ import Link from 'next/link'; import { usePerformanceMonitoring } from '@/hooks/usePerformanceMonitoring'; import { formatMetricValue } from '@/utils/performanceUtils'; import { CoreWebVitals } from './CoreWebVitals'; +import { useInternationalization } from '@/hooks/useInternationalization'; +import { translateWithFallback } from '@/components/dashboard/dashboardI18n'; const showMonitorUi = process.env.NODE_ENV !== 'production' || process.env.NEXT_PUBLIC_PERF_MONITOR_UI === 'true'; @@ -14,6 +16,7 @@ const showMonitorUi = */ const PerformanceMonitor: React.FC = () => { const { metrics } = usePerformanceMonitoring(); + const { t } = useInternationalization(); const [expanded, setExpanded] = useState(false); @@ -28,20 +31,24 @@ const PerformanceMonitor: React.FC = () => { }`} >
    - Performance + + {translateWithFallback(t, 'performance.monitor.heading', 'Performance')} +
    - Dashboard + {translateWithFallback(t, 'performance.monitor.dashboardLink', 'Dashboard')}
    @@ -53,10 +60,16 @@ const PerformanceMonitor: React.FC = () => { ) : (
      {Object.values(metrics).length === 0 ? ( -
    • Collecting vitals…
    • +
    • + {translateWithFallback(t, 'performance.monitor.collecting', 'Collecting vitals…')} +
    • ) : ( Object.values(metrics).map((metric) => (
    • diff --git a/src/locales/ar.json b/src/locales/ar.json index 04a54afd..f40c9b63 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -244,5 +244,89 @@ "ctaSubtitle": "انضم إلى آلاف المتعلمين على TeachLink اليوم", "ctaPrimary": "ابدأ الآن", "ctaSecondary": "تسجيل الدخول" + }, + "performance": { + "dashboard": { + "title": "لوحة أداء النظام", + "subtitle": "مؤشرات أداء ويب الأساسية، والتنبيهات الأخيرة، وعينات اتجاه الجلسة (المخزنة في sessionStorage).", + "navigation": "التنقل في لوحة أداء النظام", + "clearHistory": "مسح سجل الاتجاهات", + "trendsHeading": "الاتجاهات (خلال جلسة التبويب الحالية)", + "chartPlaceholder": "لا توجد عينات كافية بعد. تفاعل مع التطبيق أو أعد تحميل الصفحة لجمع البيانات.", + "chartAriaLabel": "مخطط اتجاه {{name}}", + "xAxisLabel": "عينة رقم", + "vitals": { + "heading": "مؤشرات أداء ويب الأساسية", + "ariaLabel": "مؤشرات أداء ويب الأساسية", + "waiting": "بانتظار البيانات...", + "ratings": { + "good": "جيد", + "needsImprovement": "بحاجة لتحسين", + "poor": "ضعيف" + } + }, + "statusPanel": { + "heading": "حالة التحليلات", + "statusLabel": "الحالة", + "active": "نشط", + "disabled": "معطل (بيئة التطوير)", + "endpoint": "نقطة النهاية", + "simulated": "المتوسط العالمي المحاكى (7 أيام)" + }, + "alertsPanel": { + "heading": "التنبيهات", + "clearList": "مسح القائمة", + "empty": "لا توجد تنبيهات تراجع في الأداء خلال هذه الجلسة." + } + }, + "monitor": { + "heading": "الأداء", + "dashboardLink": "لوحة التحكم", + "compact": "موجز", + "expand": "توسيع", + "ariaLabel": "أحدث مؤشرات الأداء", + "collecting": "جاري جمع البيانات..." + }, + "telemetry": { + "alerts": { + "poor": "{{name}} في النطاق \"الضعيف\" ({{value}}). يرجى التفكير في التحسين.", + "warning": "{{name}} بحاجة إلى تحسين ({{value}})." + }, + "suggestions": { + "impact": "تأثير {{impact}}", + "impactLevels": { + "high": "عالٍ", + "medium": "متوسط", + "low": "منخفض" + }, + "heading": "اقتراحات تحسين الأداء", + "headingFallback": "اقتراحات", + "empty": "لم يتم رصد أي مشاكل في مؤشرات الأداء الأساسية الحالية. واصل المراقبة مع تفاعل المستخدمين.", + "lcp": { + "title": "تسريع رسم أكبر محتوى مرئي (LCP)", + "detail": "قم بتحسين عنصر LCP: استخدم صوراً متجاوبة، وتنسيقات حديثة (AVIF/WebP)، وعرضاً وارتفاعاً محددين، وقم بتحميل مورد البطل مسبقاً. قلل وقت استجابة الخادم والبرامج النصية التي تعوق الرسم." + }, + "inp": { + "title": "تحسين استجابة التفاعل (INP)", + "detail": "قم بتجزئة المهام الطويلة، وتأجيل ملفات JavaScript غير الهامة، وتجنب التحديثات المتزامنة الكبيرة في معالجات الأحداث. يفضل استخدام الانتقالات وهياكل المكونات الأصغر." + }, + "cls": { + "title": "تثبيت تخطيط الصفحة (CLS)", + "detail": "احجز مساحة للصور، وعناصر التضمين، والإعلانات. تجنب إدراج محتوى فوق المحتوى الحالي بدون عنصر نائب. يفضل استخدام رسوم التحويل بدلاً من الخصائص التي تؤدي لتغيير التخطيط." + }, + "fcp": { + "title": "الوصول إلى أول رسم مرئي أسرع (FCP)", + "detail": "قم بتضمين ملفات CSS الهامة في السطر، وتقليص ملفات CSS غير المستخدمة، وتقليل حظر الخطوط. حافظ على صغر حجم أصول المسار الحرج." + }, + "ttfb": { + "title": "تقليل وقت الوصول لأول بايت (TTFB)", + "detail": "حسن التخزين المؤقت، وحسن زمن استجابة الخادم/واجهة البرمجيات، واستخدم شبكة توصيل المحتوى (CDN) للأصول الثابتة والديناميكية." + }, + "slowConnection": { + "title": "التكيف مع الشبكات البطيئة", + "detail": "تم رصد اتصال مقيد. قلل أحجام البيانات، وحمّل المحتوى أسفل الجزء المرئي ببطء، وتجنب التحميل المسبق المكثف." + } + } + } } } diff --git a/src/locales/en.json b/src/locales/en.json index 2a406e11..d09d2232 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -244,5 +244,89 @@ "ctaSubtitle": "Join thousands of learners on TeachLink today", "ctaPrimary": "Get Started", "ctaSecondary": "Sign In" + }, + "performance": { + "dashboard": { + "title": "Performance Dashboard", + "subtitle": "Core Web Vitals, recent alerts, and session trend samples (stored in sessionStorage).", + "navigation": "Performance dashboard navigation", + "clearHistory": "Clear trend history", + "trendsHeading": "Trends (this tab session)", + "chartPlaceholder": "Not enough samples yet. Interact with the app or reload to collect points.", + "chartAriaLabel": "{{name}} trend chart", + "xAxisLabel": "Sample #", + "vitals": { + "heading": "Core Web Vitals", + "ariaLabel": "Core Web Vitals", + "waiting": "Waiting…", + "ratings": { + "good": "Good", + "needsImprovement": "Needs Improvement", + "poor": "Poor" + } + }, + "statusPanel": { + "heading": "Analytics Status", + "statusLabel": "Status", + "active": "Active", + "disabled": "Disabled (Dev)", + "endpoint": "Endpoint", + "simulated": "Simulated Global Average (7d)" + }, + "alertsPanel": { + "heading": "Alerts", + "clearList": "Clear list", + "empty": "No degradation alerts in this session." + } + }, + "monitor": { + "heading": "Performance", + "dashboardLink": "Dashboard", + "compact": "Compact", + "expand": "Expand", + "ariaLabel": "Latest performance metrics", + "collecting": "Collecting vitals…" + }, + "telemetry": { + "alerts": { + "poor": "{{name}} is in the \"poor\" range ({{value}}). Consider optimization.", + "warning": "{{name}} needs improvement ({{value}})." + }, + "suggestions": { + "impact": "{{impact}} impact", + "impactLevels": { + "high": "High", + "medium": "Medium", + "low": "Low" + }, + "heading": "Optimization suggestions", + "headingFallback": "Suggestions", + "empty": "No issues detected from current Core Web Vitals. Keep monitoring as users interact with the app.", + "lcp": { + "title": "Speed up largest contentful paint", + "detail": "Optimize the LCP element: use responsive images, modern formats (AVIF/WebP), explicit width/height, and preload the hero resource. Reduce server response and render-blocking scripts." + }, + "inp": { + "title": "Improve interaction responsiveness", + "detail": "Break up long tasks, defer non-critical JavaScript, and avoid large synchronous updates in event handlers. Prefer transitions and smaller component trees on interaction paths." + }, + "cls": { + "title": "Stabilize layout", + "detail": "Reserve space for images, embeds, and ads. Avoid inserting content above existing content without a placeholder. Prefer transform animations over properties that trigger layout." + }, + "fcp": { + "title": "Reach first paint sooner", + "detail": "Inline critical CSS, trim unused CSS, and reduce font-blocking. Keep HTML and critical path assets small and cacheable." + }, + "ttfb": { + "title": "Reduce time to first byte", + "detail": "Improve edge caching, optimize server/API latency, and use a CDN for static and dynamic assets where possible." + }, + "slowConnection": { + "title": "Adapt for slow networks", + "detail": "Detected a constrained connection. Reduce payload sizes, lazy-load below-the-fold content, and avoid aggressive prefetching." + } + } + } } } diff --git a/src/locales/es.json b/src/locales/es.json index d3eee9d0..4d354b37 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -244,5 +244,89 @@ "ctaSubtitle": "Join thousands of learners on TeachLink today", "ctaPrimary": "Get Started", "ctaSecondary": "Sign In" + }, + "performance": { + "dashboard": { + "title": "Panel de rendimiento", + "subtitle": "Core Web Vitals, alertas recientes y muestras de tendencias de la sesión (almacenadas en sessionStorage).", + "navigation": "Navegación del panel de rendimiento", + "clearHistory": "Borrar historial de tendencias", + "trendsHeading": "Tendencias (esta sesión de pestaña)", + "chartPlaceholder": "Aún no hay suficientes muestras. Interactúe con la aplicación o vuelva a cargar para recopilar puntos.", + "chartAriaLabel": "Gráfico de tendencia de {{name}}", + "xAxisLabel": "Muestra #", + "vitals": { + "heading": "Core Web Vitals", + "ariaLabel": "Core Web Vitals", + "waiting": "Esperando...", + "ratings": { + "good": "Bueno", + "needsImprovement": "Necesita mejorar", + "poor": "Deficiente" + } + }, + "statusPanel": { + "heading": "Estado de analíticas", + "statusLabel": "Estado", + "active": "Activo", + "disabled": "Deshabilitado (Desarrollo)", + "endpoint": "Punto final", + "simulated": "Promedio global simulado (7d)" + }, + "alertsPanel": { + "heading": "Alertas", + "clearList": "Borrar lista", + "empty": "Sin alertas de degradación en esta sesión." + } + }, + "monitor": { + "heading": "Rendimiento", + "dashboardLink": "Panel", + "compact": "Compacto", + "expand": "Expandir", + "ariaLabel": "Últimas métricas de rendimiento", + "collecting": "Recopilando métricas..." + }, + "telemetry": { + "alerts": { + "poor": "{{name}} está en el rango \"deficiente\" ({{value}}). Considere la optimización.", + "warning": "{{name}} necesita mejorar ({{value}})." + }, + "suggestions": { + "impact": "Impacto {{impact}}", + "impactLevels": { + "high": "Alto", + "medium": "Medio", + "low": "Bajo" + }, + "heading": "Sugerencias de optimización", + "headingFallback": "Sugerencias", + "empty": "No se detectaron problemas a partir de las Core Web Vitals actuales. Siga monitoreando mientras los usuarios interactúan con la aplicación.", + "lcp": { + "title": "Acelerar el despliegue del elemento visual más grande (LCP)", + "detail": "Optimice el elemento LCP: use imágenes adaptables, formatos modernos (AVIF/WebP), ancho/alto explícitos y cargue previamente el recurso hero. Reduzca el tiempo de respuesta del servidor y los scripts que bloquean el renderizado." + }, + "inp": { + "title": "Mejorar la capacidad de respuesta a la interacción (INP)", + "detail": "Divida las tareas largas, aplace el JavaScript no crítico y evite grandes actualizaciones síncronas en los manejadores de eventos. Prefiera transiciones y árboles de componentes más pequeños en las rutas de interacción." + }, + "cls": { + "title": "Estabilizar el diseño (CLS)", + "detail": "Reserve espacio para imágenes, elementos incrustados y anuncios. Evite insertar contenido encima del contenido existente sin un marcador de posición. Prefiera animaciones de transformación en lugar de propiedades que desencadenen el diseño." + }, + "fcp": { + "title": "Lograr el primer renderizado antes (FCP)", + "detail": "Inserte CSS crítico en línea, recorte el CSS no utilizado y reduzca el bloqueo de fuentes. Mantenga los recursos del path crítico y del HTML pequeños y almacenables en caché." + }, + "ttfb": { + "title": "Reducir el tiempo al primer byte (TTFB)", + "detail": "Mejore el almacenamiento en caché perimetral, optimice la latencia del servidor/API y use una CDN para recursos estáticos y dinámicos donde sea posible." + }, + "slowConnection": { + "title": "Adaptarse a redes lentas", + "detail": "Se detectó una conexión limitada. Reduzca el tamaño de las cargas útiles, cargue de forma diferida el contenido debajo de la parte visible y evite la precarga agresiva." + } + } + } } }