diff --git a/frontend/scripts/check-i18n.js b/frontend/scripts/check-i18n.js
index 06b5d8f23..ebe704855 100644
--- a/frontend/scripts/check-i18n.js
+++ b/frontend/scripts/check-i18n.js
@@ -131,6 +131,12 @@ async function extractSourceKeys() {
for (const match of matches) {
keys.add(match[1]);
}
+ const metadataMatches = content.matchAll(
+ /\b(?:name|description|label|title|subtitle)Key:\s*['"]([a-zA-Z][a-zA-Z0-9_.]+)['"]/g
+ );
+ for (const match of metadataMatches) {
+ keys.add(match[1]);
+ }
}
return keys;
diff --git a/frontend/src/lib/layout/DashboardCustomizationSidebar.svelte b/frontend/src/lib/layout/DashboardCustomizationSidebar.svelte
index 78027d114..8cb0834ad 100644
--- a/frontend/src/lib/layout/DashboardCustomizationSidebar.svelte
+++ b/frontend/src/lib/layout/DashboardCustomizationSidebar.svelte
@@ -29,25 +29,26 @@
const categories = [
{
id: dashboardWidgetCategories.ACTIVITY,
- name: 'Activity',
+ nameKey: 'dashboard.customization.activity.name',
icon: Clock,
- description: 'Briefings, activity streams, and notifications',
+ descriptionKey: 'dashboard.customization.activity.description',
},
{
id: dashboardWidgetCategories.WORK,
- name: 'Work',
+ nameKey: 'dashboard.customization.work.name',
icon: CheckSquare,
- description: 'Items, milestones, and things assigned to you',
+ descriptionKey: 'dashboard.customization.work.description',
},
{
id: dashboardWidgetCategories.NAVIGATION,
- name: 'Navigation',
+ nameKey: 'dashboard.customization.navigation.name',
icon: Compass,
- description: 'Quick access to workspaces',
+ descriptionKey: 'dashboard.customization.navigation.description',
},
];
let currentWidgets = $derived(getDashboardWidgetsByCategory(activeCategory));
+ let currentCategory = $derived(categories.find((category) => category.id === activeCategory));
function handleKeydown(event) {
if (event.key === 'Escape' && isOpen) {
@@ -90,7 +91,8 @@
if (!isActive) e.currentTarget.style.cssText = 'color: var(--ds-text-subtle);';
}}
onclick={() => (activeCategory = category.id)}
- title={category.name}
+ title={t(category.nameKey)}
+ aria-label={t(category.nameKey)}
>
@@ -100,8 +102,8 @@
c.id === activeCategory)?.name || 'Widgets'}
- subtitle={categories.find((c) => c.id === activeCategory)?.description || ''}
+ title={currentCategory ? t(currentCategory.nameKey) : t('dashboard.customization.widgets')}
+ subtitle={currentCategory ? t(currentCategory.descriptionKey) : ''}
onClose={() => (isOpen = false)}
/>
@@ -130,14 +132,14 @@
-
{widget.name}
-
{widget.description}
+
{t(widget.nameKey)}
+
{t(widget.descriptionKey)}
- {widget.category}
+ {t(`dashboard.customization.${widget.category}.name`)}
{t('widgets.defaultWidth', {
@@ -163,7 +165,8 @@
style="background-color: var(--ds-background-neutral); border: 1px solid var(--ds-border);"
>
- Tip: Drag widgets from here into any section on your dashboard.
+ {t('dashboard.customization.tipLabel')}:
+ {t('dashboard.customization.tip')}
diff --git a/frontend/src/lib/locales/ar/dashboard.js b/frontend/src/lib/locales/ar/dashboard.js
new file mode 100644
index 000000000..04ce72a2a
--- /dev/null
+++ b/frontend/src/lib/locales/ar/dashboard.js
@@ -0,0 +1,118 @@
+export default {
+ dashboard: {
+ salutation: {
+ withName: '{salutation}، {name}!',
+ withoutName: '{salutation}!',
+ },
+ sections: {
+ yourDay: {
+ title: 'يومك',
+ subtitle: 'نظرة سريعة على ما يحتاج إلى انتباهك',
+ },
+ work: {
+ title: 'العمل',
+ subtitle: 'قائمتك الشخصية والعناصر المسندة إليك',
+ },
+ workspaces: {
+ title: 'مساحات العمل',
+ subtitle: 'تابع من حيث توقفت',
+ },
+ },
+ widgetCatalog: {
+ dailyBriefing: {
+ name: 'الموجز اليومي',
+ description: 'ملخص مولد بالذكاء الاصطناعي لما يهمك اليوم',
+ },
+ yourActivity: {
+ name: 'نشاطك',
+ description: 'العناصر التي عرضتها أو عدلتها أو علقت عليها مؤخراً',
+ },
+ whatsNew: {
+ name: 'ما الجديد',
+ description: 'أحدث الإشعارات والتحديثات غير المقروءة',
+ },
+ personalTasks: {
+ name: 'المهام الشخصية',
+ description: 'عناصر من قائمة مهامك الشخصية',
+ },
+ savedSearch: {
+ name: 'بحث محفوظ',
+ description: 'عرض عناصر العمل من مجموعة محفوظة',
+ },
+ assignedToMe: {
+ name: 'مسند إليّ',
+ description: 'العناصر المفتوحة المسندة إليك في جميع مساحات العمل',
+ },
+ watchedItems: {
+ name: 'العناصر المراقبة',
+ description: 'العناصر التي تتابعها',
+ },
+ upcomingMilestones: {
+ name: 'المعالم القادمة',
+ description: 'المعالم ذات التواريخ المستهدفة القريبة',
+ },
+ recentWorkspaces: {
+ name: 'مساحات العمل الأخيرة',
+ description: 'مساحات العمل التي زرتها مؤخراً',
+ },
+ quickAccess: {
+ name: 'الوصول السريع',
+ description: 'روابط سريعة إلى مساحات العمل المتاحة لك',
+ },
+ },
+ customization: {
+ widgets: 'الأدوات',
+ activity: {
+ name: 'النشاط',
+ description: 'الموجزات وتدفقات النشاط والإشعارات',
+ },
+ work: {
+ name: 'العمل',
+ description: 'العناصر والمعالم والمهام المسندة إليك',
+ },
+ navigation: {
+ name: 'التنقل',
+ description: 'وصول سريع إلى مساحات العمل',
+ },
+ tipLabel: 'تلميح',
+ tip: 'اسحب الأدوات من هنا إلى أي قسم في لوحة المعلومات.',
+ },
+ editor: {
+ newSection: 'قسم جديد',
+ deleteSectionConfirm: 'هل تريد حذف هذا القسم؟ ستتم إزالة جميع الأدوات الموجودة فيه.',
+ doneEditing: 'إنهاء التحرير',
+ customize: 'تخصيص',
+ editModeDescription: 'وضع التحرير: أضف الأقسام والأدوات أو أعد تسميتها أو ترتيبها أو احذفها',
+ addSection: 'إضافة قسم',
+ sectionLabel: 'قسم لوحة المعلومات',
+ sectionTitlePlaceholder: 'عنوان القسم',
+ sectionSubtitlePlaceholder: 'العنوان الفرعي (اختياري)',
+ renameSection: 'إعادة تسمية القسم',
+ deleteSection: 'حذف القسم',
+ unknownWidgetType: 'نوع أداة غير معروف: {type}',
+ noWidgets: 'لا توجد أدوات في هذا القسم بعد',
+ addWidgetsHint: 'اختر تخصيص لإضافة أدوات',
+ noSections: 'لم يتم إعداد أي أقسام',
+ addSectionsHint: 'اختر تحرير لإضافة أقسام إلى لوحة المعلومات',
+ },
+ states: {
+ assignedLoadError: 'تعذر تحميل العناصر المسندة إليك',
+ assignedEmpty: 'لا يوجد شيء مسند إليك حالياً',
+ personalTasksLoadError: 'تعذر تحميل مهامك الشخصية',
+ personalTasksEmpty: 'قائمة مهامك الشخصية فارغة',
+ dailyBriefingUnavailable: 'موجزك اليومي غير متاح حالياً. يعتمد على تكامل للذكاء الاصطناعي. إذا أعددت تكاملاً للتو، فحاول مرة أخرى بعد قليل.',
+ updatedAt: 'تم التحديث في {time}',
+ priorityLabel: 'الأولوية: {priority}',
+ noWorkspaces: 'لا توجد مساحات عمل بعد',
+ createWorkspace: 'إنشاء مساحة',
+ workspaceAvatarAlt: 'الصورة الرمزية لـ {name}',
+ visited: 'تمت الزيارة {time}',
+ noUpcomingMilestones: 'لا توجد معالم قادمة',
+ milestoneProgress: 'تم إنجاز {done} من {total}',
+ daysOverdue: 'متأخر {days} أيام',
+ daysLeft: 'متبقي {days} أيام',
+ watchedItemsEmpty: 'أنت لا تراقب أي عناصر',
+ workspaceWithId: 'مساحة العمل {id}',
+ },
+ },
+};
diff --git a/frontend/src/lib/locales/ar/index.js b/frontend/src/lib/locales/ar/index.js
index 19ae66317..4faeae034 100644
--- a/frontend/src/lib/locales/ar/index.js
+++ b/frontend/src/lib/locales/ar/index.js
@@ -21,6 +21,7 @@ import workspace from './workspace.js';
import pages from './pages.js';
import teams from './teams.js';
import supplemental from './supplemental.js';
+import dashboard from './dashboard.js';
export default createLocale({
common,
@@ -40,4 +41,5 @@ export default createLocale({
pages,
teams,
supplemental,
+ dashboard,
});
diff --git a/frontend/src/lib/locales/de/dashboard.js b/frontend/src/lib/locales/de/dashboard.js
new file mode 100644
index 000000000..d40147b3f
--- /dev/null
+++ b/frontend/src/lib/locales/de/dashboard.js
@@ -0,0 +1,118 @@
+export default {
+ dashboard: {
+ salutation: {
+ withName: '{salutation}, {name}!',
+ withoutName: '{salutation}!',
+ },
+ sections: {
+ yourDay: {
+ title: 'Ihr Tag',
+ subtitle: 'Ein schneller Überblick darüber, was Ihre Aufmerksamkeit braucht',
+ },
+ work: {
+ title: 'Arbeit',
+ subtitle: 'Ihre persönliche Liste und die Ihnen zugewiesenen Einträge',
+ },
+ workspaces: {
+ title: 'Arbeitsbereiche',
+ subtitle: 'Direkt weitermachen',
+ },
+ },
+ widgetCatalog: {
+ dailyBriefing: {
+ name: 'Täglicher Überblick',
+ description: 'KI-generierte Zusammenfassung dessen, was heute für Sie wichtig ist',
+ },
+ yourActivity: {
+ name: 'Ihre Aktivität',
+ description: 'Kürzlich angesehene, bearbeitete oder kommentierte Einträge',
+ },
+ whatsNew: {
+ name: 'Neuigkeiten',
+ description: 'Neueste Benachrichtigungen und ungelesene Aktualisierungen',
+ },
+ personalTasks: {
+ name: 'Persönliche Aufgaben',
+ description: 'Einträge aus Ihrer persönlichen Aufgabenliste',
+ },
+ savedSearch: {
+ name: 'Gespeicherte Suche',
+ description: 'Einträge aus einer gespeicherten Sammlung anzeigen',
+ },
+ assignedToMe: {
+ name: 'Mir zugewiesen',
+ description: 'Ihnen zugewiesene offene Einträge aus allen Arbeitsbereichen',
+ },
+ watchedItems: {
+ name: 'Beobachtete Einträge',
+ description: 'Einträge, denen Sie folgen',
+ },
+ upcomingMilestones: {
+ name: 'Anstehende Meilensteine',
+ description: 'Meilensteine mit nahendem Zieldatum',
+ },
+ recentWorkspaces: {
+ name: 'Letzte Arbeitsbereiche',
+ description: 'Kürzlich besuchte Arbeitsbereiche',
+ },
+ quickAccess: {
+ name: 'Schnellzugriff',
+ description: 'Direkte Verknüpfungen zu erreichbaren Arbeitsbereichen',
+ },
+ },
+ customization: {
+ widgets: 'Widgets',
+ activity: {
+ name: 'Aktivität',
+ description: 'Überblicke, Aktivitätsverläufe und Benachrichtigungen',
+ },
+ work: {
+ name: 'Arbeit',
+ description: 'Einträge, Meilensteine und Ihnen zugewiesene Aufgaben',
+ },
+ navigation: {
+ name: 'Navigation',
+ description: 'Schnellzugriff auf Arbeitsbereiche',
+ },
+ tipLabel: 'Tipp',
+ tip: 'Ziehen Sie Widgets von hier in einen beliebigen Bereich Ihres Dashboards.',
+ },
+ editor: {
+ newSection: 'Neuer Bereich',
+ deleteSectionConfirm: 'Diesen Bereich löschen? Alle darin enthaltenen Widgets werden entfernt.',
+ doneEditing: 'Bearbeitung beenden',
+ customize: 'Anpassen',
+ editModeDescription: 'Bearbeitungsmodus: Bereiche und Widgets hinzufügen, umbenennen, neu anordnen oder löschen',
+ addSection: 'Bereich hinzufügen',
+ sectionLabel: 'Dashboard-Bereich',
+ sectionTitlePlaceholder: 'Bereichstitel',
+ sectionSubtitlePlaceholder: 'Untertitel (optional)',
+ renameSection: 'Bereich umbenennen',
+ deleteSection: 'Bereich löschen',
+ unknownWidgetType: 'Unbekannter Widget-Typ: {type}',
+ noWidgets: 'Dieser Bereich enthält noch keine Widgets',
+ addWidgetsHint: 'Wählen Sie Anpassen, um Widgets hinzuzufügen',
+ noSections: 'Keine Bereiche konfiguriert',
+ addSectionsHint: 'Wählen Sie Bearbeiten, um Bereiche zum Dashboard hinzuzufügen',
+ },
+ states: {
+ assignedLoadError: 'Die Ihnen zugewiesenen Einträge konnten nicht geladen werden',
+ assignedEmpty: 'Ihnen ist derzeit nichts zugewiesen',
+ personalTasksLoadError: 'Ihre persönlichen Aufgaben konnten nicht geladen werden',
+ personalTasksEmpty: 'Ihre persönliche Aufgabenliste ist leer',
+ dailyBriefingUnavailable: 'Ihr täglicher Überblick ist derzeit nicht verfügbar. Er benötigt eine KI-Integration. Falls Sie gerade eine eingerichtet haben, versuchen Sie es in Kürze erneut.',
+ updatedAt: 'Aktualisiert: {time}',
+ priorityLabel: 'Priorität: {priority}',
+ noWorkspaces: 'Noch keine Arbeitsbereiche',
+ createWorkspace: 'Arbeitsbereich erstellen',
+ workspaceAvatarAlt: 'Avatar von {name}',
+ visited: 'besucht {time}',
+ noUpcomingMilestones: 'Keine anstehenden Meilensteine',
+ milestoneProgress: '{done} von {total} erledigt',
+ daysOverdue: '{days} Tage überfällig',
+ daysLeft: 'Noch {days} Tage',
+ watchedItemsEmpty: 'Sie beobachten derzeit keine Einträge',
+ workspaceWithId: 'Arbeitsbereich {id}',
+ },
+ },
+};
diff --git a/frontend/src/lib/locales/de/index.js b/frontend/src/lib/locales/de/index.js
index 809106aec..18317899a 100644
--- a/frontend/src/lib/locales/de/index.js
+++ b/frontend/src/lib/locales/de/index.js
@@ -23,6 +23,7 @@ import pages from './pages.js';
import supplemental from './supplemental.js';
import quality from './quality.js';
import review from './review.js';
+import dashboard from './dashboard.js';
export default createLocale({
admin,
@@ -44,4 +45,5 @@ export default createLocale({
supplemental,
quality,
review,
+ dashboard,
});
diff --git a/frontend/src/lib/locales/en/dashboard.js b/frontend/src/lib/locales/en/dashboard.js
new file mode 100644
index 000000000..0963d6295
--- /dev/null
+++ b/frontend/src/lib/locales/en/dashboard.js
@@ -0,0 +1,118 @@
+export default {
+ dashboard: {
+ salutation: {
+ withName: '{salutation}, {name}!',
+ withoutName: '{salutation}!',
+ },
+ sections: {
+ yourDay: {
+ title: 'Your Day',
+ subtitle: 'A quick read on what needs your attention',
+ },
+ work: {
+ title: 'Work',
+ subtitle: 'Your personal list and items assigned to you',
+ },
+ workspaces: {
+ title: 'Workspaces',
+ subtitle: 'Jump back in',
+ },
+ },
+ widgetCatalog: {
+ dailyBriefing: {
+ name: 'Daily Briefing',
+ description: 'AI-generated summary of what matters to you today',
+ },
+ yourActivity: {
+ name: 'Your Activity',
+ description: 'Items you recently viewed, edited, or commented on',
+ },
+ whatsNew: {
+ name: "What's New",
+ description: 'Latest notifications and unread updates',
+ },
+ personalTasks: {
+ name: 'Personal Tasks',
+ description: 'Items from your personal todo list',
+ },
+ savedSearch: {
+ name: 'Saved Search',
+ description: 'Display work items from a saved collection',
+ },
+ assignedToMe: {
+ name: 'Assigned to Me',
+ description: 'Open items assigned to you across all workspaces',
+ },
+ watchedItems: {
+ name: 'Watched Items',
+ description: 'Items you are following',
+ },
+ upcomingMilestones: {
+ name: 'Upcoming Milestones',
+ description: 'Milestones with approaching target dates',
+ },
+ recentWorkspaces: {
+ name: 'Recent Workspaces',
+ description: 'Workspaces you recently visited',
+ },
+ quickAccess: {
+ name: 'Quick Access',
+ description: 'Quick links to workspaces you can reach',
+ },
+ },
+ customization: {
+ widgets: 'Widgets',
+ activity: {
+ name: 'Activity',
+ description: 'Briefings, activity streams, and notifications',
+ },
+ work: {
+ name: 'Work',
+ description: 'Items, milestones, and things assigned to you',
+ },
+ navigation: {
+ name: 'Navigation',
+ description: 'Quick access to workspaces',
+ },
+ tipLabel: 'Tip',
+ tip: 'Drag widgets from here into any section on your dashboard.',
+ },
+ editor: {
+ newSection: 'New Section',
+ deleteSectionConfirm: 'Delete this section? All widgets in this section will be removed.',
+ doneEditing: 'Done Editing',
+ customize: 'Customize',
+ editModeDescription: 'Edit mode: add, rename, reorder, or delete sections and widgets',
+ addSection: 'Add Section',
+ sectionLabel: 'Dashboard section',
+ sectionTitlePlaceholder: 'Section title',
+ sectionSubtitlePlaceholder: 'Subtitle (optional)',
+ renameSection: 'Rename section',
+ deleteSection: 'Delete section',
+ unknownWidgetType: 'Unknown widget type: {type}',
+ noWidgets: 'No widgets in this section yet',
+ addWidgetsHint: 'Select Customize to add widgets',
+ noSections: 'No sections configured',
+ addSectionsHint: 'Select Edit to add sections to your dashboard',
+ },
+ states: {
+ assignedLoadError: "Couldn't load your assigned items",
+ assignedEmpty: 'Nothing assigned to you right now',
+ personalTasksLoadError: "Couldn't load your personal tasks",
+ personalTasksEmpty: 'Your personal todo list is empty',
+ dailyBriefingUnavailable: "Your daily briefing isn't available right now. It relies on an AI integration. If you've just set one up, check back in a bit.",
+ updatedAt: 'Updated {time}',
+ priorityLabel: 'Priority: {priority}',
+ noWorkspaces: 'No workspaces yet',
+ createWorkspace: 'Create one',
+ workspaceAvatarAlt: '{name} avatar',
+ visited: 'visited {time}',
+ noUpcomingMilestones: 'No upcoming milestones',
+ milestoneProgress: '{done} of {total} done',
+ daysOverdue: '{days} days overdue',
+ daysLeft: '{days} days left',
+ watchedItemsEmpty: "You aren't watching any items",
+ workspaceWithId: 'Workspace {id}',
+ },
+ },
+};
diff --git a/frontend/src/lib/locales/en/index.js b/frontend/src/lib/locales/en/index.js
index eac42aba1..8fe81d6d8 100644
--- a/frontend/src/lib/locales/en/index.js
+++ b/frontend/src/lib/locales/en/index.js
@@ -20,6 +20,7 @@ import ui from './ui.js';
import workflows from './workflows.js';
import workspace from './workspace.js';
import pages from './pages.js';
+import dashboard from './dashboard.js';
export default createLocale({
common,
@@ -38,4 +39,5 @@ export default createLocale({
analytics,
teams,
pages,
+ dashboard,
});
diff --git a/frontend/src/lib/locales/es/dashboard.js b/frontend/src/lib/locales/es/dashboard.js
new file mode 100644
index 000000000..ad5997d2b
--- /dev/null
+++ b/frontend/src/lib/locales/es/dashboard.js
@@ -0,0 +1,118 @@
+export default {
+ dashboard: {
+ salutation: {
+ withName: '¡{salutation}, {name}!',
+ withoutName: '¡{salutation}!',
+ },
+ sections: {
+ yourDay: {
+ title: 'Tu día',
+ subtitle: 'Un resumen rápido de lo que necesita tu atención',
+ },
+ work: {
+ title: 'Trabajo',
+ subtitle: 'Tu lista personal y los elementos que tienes asignados',
+ },
+ workspaces: {
+ title: 'Espacios de trabajo',
+ subtitle: 'Retoma el trabajo',
+ },
+ },
+ widgetCatalog: {
+ dailyBriefing: {
+ name: 'Resumen diario',
+ description: 'Resumen generado por IA de lo que te importa hoy',
+ },
+ yourActivity: {
+ name: 'Tu actividad',
+ description: 'Elementos que has visto, editado o comentado recientemente',
+ },
+ whatsNew: {
+ name: 'Novedades',
+ description: 'Últimas notificaciones y actualizaciones sin leer',
+ },
+ personalTasks: {
+ name: 'Tareas personales',
+ description: 'Elementos de tu lista personal de tareas',
+ },
+ savedSearch: {
+ name: 'Búsqueda guardada',
+ description: 'Muestra elementos de una colección guardada',
+ },
+ assignedToMe: {
+ name: 'Asignados a mí',
+ description: 'Elementos abiertos que tienes asignados en todos los espacios de trabajo',
+ },
+ watchedItems: {
+ name: 'Elementos observados',
+ description: 'Elementos que sigues',
+ },
+ upcomingMilestones: {
+ name: 'Próximos hitos',
+ description: 'Hitos cuya fecha objetivo se acerca',
+ },
+ recentWorkspaces: {
+ name: 'Espacios de trabajo recientes',
+ description: 'Espacios de trabajo que has visitado recientemente',
+ },
+ quickAccess: {
+ name: 'Acceso rápido',
+ description: 'Enlaces rápidos a los espacios de trabajo disponibles',
+ },
+ },
+ customization: {
+ widgets: 'Widgets',
+ activity: {
+ name: 'Actividad',
+ description: 'Resúmenes, flujos de actividad y notificaciones',
+ },
+ work: {
+ name: 'Trabajo',
+ description: 'Elementos, hitos y tareas que tienes asignados',
+ },
+ navigation: {
+ name: 'Navegación',
+ description: 'Acceso rápido a los espacios de trabajo',
+ },
+ tipLabel: 'Consejo',
+ tip: 'Arrastra widgets desde aquí a cualquier sección de tu panel.',
+ },
+ editor: {
+ newSection: 'Nueva sección',
+ deleteSectionConfirm: '¿Eliminar esta sección? Se quitarán todos los widgets que contiene.',
+ doneEditing: 'Terminar edición',
+ customize: 'Personalizar',
+ editModeDescription: 'Modo de edición: añade, cambia el nombre, reordena o elimina secciones y widgets',
+ addSection: 'Añadir sección',
+ sectionLabel: 'Sección del panel',
+ sectionTitlePlaceholder: 'Título de la sección',
+ sectionSubtitlePlaceholder: 'Subtítulo (opcional)',
+ renameSection: 'Cambiar nombre de la sección',
+ deleteSection: 'Eliminar sección',
+ unknownWidgetType: 'Tipo de widget desconocido: {type}',
+ noWidgets: 'Esta sección aún no tiene widgets',
+ addWidgetsHint: 'Selecciona Personalizar para añadir widgets',
+ noSections: 'No hay secciones configuradas',
+ addSectionsHint: 'Selecciona Editar para añadir secciones al panel',
+ },
+ states: {
+ assignedLoadError: 'No se pudieron cargar los elementos que tienes asignados',
+ assignedEmpty: 'No tienes nada asignado en este momento',
+ personalTasksLoadError: 'No se pudieron cargar tus tareas personales',
+ personalTasksEmpty: 'Tu lista personal de tareas está vacía',
+ dailyBriefingUnavailable: 'Tu resumen diario no está disponible en este momento. Necesita una integración de IA. Si acabas de configurar una, vuelve a intentarlo en unos instantes.',
+ updatedAt: 'Actualizado {time}',
+ priorityLabel: 'Prioridad: {priority}',
+ noWorkspaces: 'Aún no hay espacios de trabajo',
+ createWorkspace: 'Crear uno',
+ workspaceAvatarAlt: 'Avatar de {name}',
+ visited: 'visitado {time}',
+ noUpcomingMilestones: 'No hay próximos hitos',
+ milestoneProgress: '{done} de {total} completados',
+ daysOverdue: '{days} días de retraso',
+ daysLeft: 'Quedan {days} días',
+ watchedItemsEmpty: 'No estás observando ningún elemento',
+ workspaceWithId: 'Espacio de trabajo {id}',
+ },
+ },
+};
diff --git a/frontend/src/lib/locales/es/index.js b/frontend/src/lib/locales/es/index.js
index 543044c69..0924d6e98 100644
--- a/frontend/src/lib/locales/es/index.js
+++ b/frontend/src/lib/locales/es/index.js
@@ -22,6 +22,7 @@ import pages from './pages.js';
import supplemental from './supplemental.js';
import quality from './quality.js';
import review from './review.js';
+import dashboard from './dashboard.js';
export default createLocale({
common,
@@ -43,4 +44,5 @@ export default createLocale({
supplemental,
quality,
review,
+ dashboard,
});
diff --git a/frontend/src/lib/locales/pt-BR/dashboard.js b/frontend/src/lib/locales/pt-BR/dashboard.js
new file mode 100644
index 000000000..88c0d8915
--- /dev/null
+++ b/frontend/src/lib/locales/pt-BR/dashboard.js
@@ -0,0 +1,118 @@
+export default {
+ dashboard: {
+ salutation: {
+ withName: '{salutation}, {name}!',
+ withoutName: '{salutation}!',
+ },
+ sections: {
+ yourDay: {
+ title: 'Seu dia',
+ subtitle: 'Uma visão rápida do que precisa da sua atenção',
+ },
+ work: {
+ title: 'Trabalho',
+ subtitle: 'Sua lista pessoal e os itens atribuídos a você',
+ },
+ workspaces: {
+ title: 'Espaços de trabalho',
+ subtitle: 'Continue de onde parou',
+ },
+ },
+ widgetCatalog: {
+ dailyBriefing: {
+ name: 'Resumo diário',
+ description: 'Resumo gerado por IA do que importa para você hoje',
+ },
+ yourActivity: {
+ name: 'Sua atividade',
+ description: 'Itens que você viu, editou ou comentou recentemente',
+ },
+ whatsNew: {
+ name: 'Novidades',
+ description: 'Notificações recentes e atualizações não lidas',
+ },
+ personalTasks: {
+ name: 'Tarefas pessoais',
+ description: 'Itens da sua lista pessoal de tarefas',
+ },
+ savedSearch: {
+ name: 'Pesquisa salva',
+ description: 'Exibe itens de uma coleção salva',
+ },
+ assignedToMe: {
+ name: 'Atribuídos a mim',
+ description: 'Itens abertos atribuídos a você em todos os espaços de trabalho',
+ },
+ watchedItems: {
+ name: 'Itens observados',
+ description: 'Itens que você acompanha',
+ },
+ upcomingMilestones: {
+ name: 'Próximos marcos',
+ description: 'Marcos com datas-alvo próximas',
+ },
+ recentWorkspaces: {
+ name: 'Espaços de trabalho recentes',
+ description: 'Espaços de trabalho visitados recentemente',
+ },
+ quickAccess: {
+ name: 'Acesso rápido',
+ description: 'Links rápidos para os espaços de trabalho disponíveis',
+ },
+ },
+ customization: {
+ widgets: 'Widgets',
+ activity: {
+ name: 'Atividade',
+ description: 'Resumos, fluxos de atividade e notificações',
+ },
+ work: {
+ name: 'Trabalho',
+ description: 'Itens, marcos e tarefas atribuídos a você',
+ },
+ navigation: {
+ name: 'Navegação',
+ description: 'Acesso rápido aos espaços de trabalho',
+ },
+ tipLabel: 'Dica',
+ tip: 'Arraste widgets daqui para qualquer seção do seu painel.',
+ },
+ editor: {
+ newSection: 'Nova seção',
+ deleteSectionConfirm: 'Excluir esta seção? Todos os widgets nela serão removidos.',
+ doneEditing: 'Concluir edição',
+ customize: 'Personalizar',
+ editModeDescription: 'Modo de edição: adicione, renomeie, reordene ou exclua seções e widgets',
+ addSection: 'Adicionar seção',
+ sectionLabel: 'Seção do painel',
+ sectionTitlePlaceholder: 'Título da seção',
+ sectionSubtitlePlaceholder: 'Subtítulo (opcional)',
+ renameSection: 'Renomear seção',
+ deleteSection: 'Excluir seção',
+ unknownWidgetType: 'Tipo de widget desconhecido: {type}',
+ noWidgets: 'Ainda não há widgets nesta seção',
+ addWidgetsHint: 'Selecione Personalizar para adicionar widgets',
+ noSections: 'Nenhuma seção configurada',
+ addSectionsHint: 'Selecione Editar para adicionar seções ao painel',
+ },
+ states: {
+ assignedLoadError: 'Não foi possível carregar os itens atribuídos a você',
+ assignedEmpty: 'Nada foi atribuído a você no momento',
+ personalTasksLoadError: 'Não foi possível carregar suas tarefas pessoais',
+ personalTasksEmpty: 'Sua lista pessoal de tarefas está vazia',
+ dailyBriefingUnavailable: 'Seu resumo diário não está disponível no momento. Ele depende de uma integração de IA. Se você acabou de configurar uma, tente novamente em instantes.',
+ updatedAt: 'Atualizado em {time}',
+ priorityLabel: 'Prioridade: {priority}',
+ noWorkspaces: 'Ainda não há espaços de trabalho',
+ createWorkspace: 'Criar um',
+ workspaceAvatarAlt: 'Avatar de {name}',
+ visited: 'visitado {time}',
+ noUpcomingMilestones: 'Nenhum marco próximo',
+ milestoneProgress: '{done} de {total} concluídos',
+ daysOverdue: '{days} dias de atraso',
+ daysLeft: 'Faltam {days} dias',
+ watchedItemsEmpty: 'Você não está observando nenhum item',
+ workspaceWithId: 'Espaço de trabalho {id}',
+ },
+ },
+};
diff --git a/frontend/src/lib/locales/pt-BR/index.js b/frontend/src/lib/locales/pt-BR/index.js
index 0bff9ba2d..c947c9530 100644
--- a/frontend/src/lib/locales/pt-BR/index.js
+++ b/frontend/src/lib/locales/pt-BR/index.js
@@ -21,6 +21,7 @@ import teams from './teams.js';
import pages from './pages.js';
import supplemental from './supplemental.js';
import quality from './quality.js';
+import dashboard from './dashboard.js';
export default createLocale({
common,
@@ -41,4 +42,5 @@ export default createLocale({
teams,
supplemental,
quality,
+ dashboard,
});
diff --git a/frontend/src/lib/locales/zh-CN/dashboard.js b/frontend/src/lib/locales/zh-CN/dashboard.js
new file mode 100644
index 000000000..5666da873
--- /dev/null
+++ b/frontend/src/lib/locales/zh-CN/dashboard.js
@@ -0,0 +1,118 @@
+export default {
+ dashboard: {
+ salutation: {
+ withName: '{salutation},{name}!',
+ withoutName: '{salutation}!',
+ },
+ sections: {
+ yourDay: {
+ title: '你的一天',
+ subtitle: '快速了解需要你关注的事项',
+ },
+ work: {
+ title: '工作',
+ subtitle: '你的个人列表和分配给你的事项',
+ },
+ workspaces: {
+ title: '工作区',
+ subtitle: '继续之前的工作',
+ },
+ },
+ widgetCatalog: {
+ dailyBriefing: {
+ name: '每日简报',
+ description: '由 AI 生成的今日重点摘要',
+ },
+ yourActivity: {
+ name: '你的活动',
+ description: '你最近查看、编辑或评论的事项',
+ },
+ whatsNew: {
+ name: '最新动态',
+ description: '最新通知和未读更新',
+ },
+ personalTasks: {
+ name: '个人任务',
+ description: '个人待办列表中的事项',
+ },
+ savedSearch: {
+ name: '已保存的搜索',
+ description: '显示已保存集合中的工作事项',
+ },
+ assignedToMe: {
+ name: '分配给我的',
+ description: '所有工作区中分配给你的未完成事项',
+ },
+ watchedItems: {
+ name: '关注的事项',
+ description: '你正在关注的事项',
+ },
+ upcomingMilestones: {
+ name: '即将到来的里程碑',
+ description: '目标日期临近的里程碑',
+ },
+ recentWorkspaces: {
+ name: '最近的工作区',
+ description: '你最近访问过的工作区',
+ },
+ quickAccess: {
+ name: '快速访问',
+ description: '快速打开你有权访问的工作区',
+ },
+ },
+ customization: {
+ widgets: '小部件',
+ activity: {
+ name: '活动',
+ description: '简报、活动记录和通知',
+ },
+ work: {
+ name: '工作',
+ description: '事项、里程碑和分配给你的任务',
+ },
+ navigation: {
+ name: '导航',
+ description: '快速访问工作区',
+ },
+ tipLabel: '提示',
+ tip: '将小部件从这里拖到面板中的任意分区。',
+ },
+ editor: {
+ newSection: '新建分区',
+ deleteSectionConfirm: '要删除此分区吗?其中的所有小部件都将被移除。',
+ doneEditing: '完成编辑',
+ customize: '自定义',
+ editModeDescription: '编辑模式:添加、重命名、重新排序或删除分区和小部件',
+ addSection: '添加分区',
+ sectionLabel: '面板分区',
+ sectionTitlePlaceholder: '分区标题',
+ sectionSubtitlePlaceholder: '副标题(可选)',
+ renameSection: '重命名分区',
+ deleteSection: '删除分区',
+ unknownWidgetType: '未知的小部件类型:{type}',
+ noWidgets: '此分区中还没有小部件',
+ addWidgetsHint: '选择“自定义”以添加小部件',
+ noSections: '尚未配置分区',
+ addSectionsHint: '选择“编辑”以向面板添加分区',
+ },
+ states: {
+ assignedLoadError: '无法加载分配给你的事项',
+ assignedEmpty: '目前没有分配给你的事项',
+ personalTasksLoadError: '无法加载你的个人任务',
+ personalTasksEmpty: '你的个人待办列表为空',
+ dailyBriefingUnavailable: '你的每日简报目前不可用。它依赖 AI 集成。如果你刚完成设置,请稍后再试。',
+ updatedAt: '更新于 {time}',
+ priorityLabel: '优先级:{priority}',
+ noWorkspaces: '还没有工作区',
+ createWorkspace: '创建一个',
+ workspaceAvatarAlt: '{name} 的头像',
+ visited: '访问于 {time}',
+ noUpcomingMilestones: '没有即将到来的里程碑',
+ milestoneProgress: '已完成 {done}/{total}',
+ daysOverdue: '逾期 {days} 天',
+ daysLeft: '剩余 {days} 天',
+ watchedItemsEmpty: '你尚未关注任何事项',
+ workspaceWithId: '工作区 {id}',
+ },
+ },
+};
diff --git a/frontend/src/lib/locales/zh-CN/index.js b/frontend/src/lib/locales/zh-CN/index.js
index b6c6cf97e..1bf658744 100644
--- a/frontend/src/lib/locales/zh-CN/index.js
+++ b/frontend/src/lib/locales/zh-CN/index.js
@@ -17,6 +17,7 @@ import teams from './teams.js';
import pages from './pages.js';
import supplemental from './supplemental.js';
import quality from './quality.js';
+import dashboard from './dashboard.js';
export default createLocale({
common,
@@ -37,4 +38,5 @@ export default createLocale({
teams,
supplemental,
quality,
+ dashboard,
});
diff --git a/frontend/src/lib/pages/DashboardOnboarding.svelte b/frontend/src/lib/pages/DashboardOnboarding.svelte
index 16527c8ca..82254de20 100644
--- a/frontend/src/lib/pages/DashboardOnboarding.svelte
+++ b/frontend/src/lib/pages/DashboardOnboarding.svelte
@@ -36,6 +36,14 @@
// Derived: determine which step is active (first incomplete step, admin path only)
let activeStep = $derived(workspaceCount === 0 ? 1 : (itemCount === 0 ? 2 : 0));
+ let welcomeText = $derived(
+ userName
+ ? t('dashboard.salutation.withName', {
+ salutation: t('onboarding.welcomeTo'),
+ name: userName,
+ })
+ : t('dashboard.salutation.withoutName', { salutation: t('onboarding.welcomeTo') })
+ );
onMount(() => {
// Check if user has dismissed the onboarding
@@ -121,7 +129,7 @@
- {t('onboarding.welcomeTo')}, {userName}!
+ {welcomeText}
{#if canCreateWorkspaces}
diff --git a/frontend/src/lib/pages/Homepage.svelte b/frontend/src/lib/pages/Homepage.svelte
index f06df5f03..aa67b5212 100644
--- a/frontend/src/lib/pages/Homepage.svelte
+++ b/frontend/src/lib/pages/Homepage.svelte
@@ -21,6 +21,8 @@
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import {
+ getDashboardSectionDisplay,
+ getDashboardSectionSaveValues,
getDashboardWidgetMetadata,
} from '../services/dashboardWidgetRegistry.js';
@@ -40,6 +42,15 @@
import SavedSearchWidget from '../widgets/dashboard/SavedSearchWidget.svelte';
let greeting = $derived(homepageStore.greeting);
+ let greetingText = $derived(greeting ? t(greeting) : '');
+ let userName = $derived(authStore.currentUser?.first_name || '');
+ let personalizedGreeting = $derived(
+ greetingText
+ ? userName
+ ? t('dashboard.salutation.withName', { salutation: greetingText, name: userName })
+ : t('dashboard.salutation.withoutName', { salutation: greetingText })
+ : ''
+ );
let currentDate = $derived(homepageStore.currentDate);
let totalWorkspaceCount = $derived(homepageStore.totalWorkspaceCount);
let totalItemCount = $derived(homepageStore.totalItemCount);
@@ -114,7 +125,7 @@
}
async function addSection() {
- const created = homepageStore.addSection('New Section', '');
+ const created = homepageStore.addSection(t('dashboard.editor.newSection'), '');
editingSectionId = created.id;
editingSectionTitle = created.title;
editingSectionSubtitle = created.subtitle;
@@ -133,18 +144,24 @@
}
function startEditingSection(section) {
+ const display = getDashboardSectionDisplay(section, t);
editingSectionId = section.id;
- editingSectionTitle = section.title;
- editingSectionSubtitle = section.subtitle || '';
+ editingSectionTitle = display.title;
+ editingSectionSubtitle = display.subtitle || '';
isNewSection = false;
}
function saveSection() {
if (!editingSectionId) return;
- homepageStore.updateSection(editingSectionId, {
+ const draft = {
title: editingSectionTitle,
subtitle: editingSectionSubtitle,
- });
+ };
+ const section = sections.find((candidate) => candidate.id === editingSectionId);
+ homepageStore.updateSection(
+ editingSectionId,
+ section ? getDashboardSectionSaveValues(section, draft, t) : draft
+ );
editingSectionId = null;
isNewSection = false;
}
@@ -170,7 +187,7 @@
async function handleDeleteSection(sectionId) {
const confirmed = await confirm({
title: t('common.delete'),
- message: 'Delete this section? All widgets in this section will be removed.',
+ message: t('dashboard.editor.deleteSectionConfirm'),
confirmText: t('common.delete'),
cancelText: t('common.cancel'),
variant: 'danger',
@@ -263,7 +280,8 @@
}
function getWidgetTitle(type) {
- return getDashboardWidgetMetadata(type)?.name || type;
+ const metadata = getDashboardWidgetMetadata(type);
+ return metadata ? t(metadata.nameKey) : type;
}
function getSectionWidgets(sectionId) {
@@ -287,7 +305,7 @@
- {greeting}, {authStore.currentUser?.first_name || 'there'}!
+ {personalizedGreeting}
{currentDate}
@@ -298,14 +316,14 @@
onclick={toggleEditMode}
dataTestid="dashboard-edit-toggle"
>
- {isEditMode ? 'Done Editing' : 'Edit'}
+ {isEditMode ? t('dashboard.editor.doneEditing') : t('common.edit')}
@@ -317,7 +335,7 @@
- Edit mode: add, rename, reorder, or delete sections and widgets
+ {t('dashboard.editor.editModeDescription')}
{/if}
@@ -363,10 +381,11 @@
{#each sections as section, sectionIndex (section.id)}
{@const sectionWidgets = getSectionWidgets(section.id)}
+ {@const sectionDisplay = getDashboardSectionDisplay(section, t)}
@@ -377,22 +396,22 @@
type="text"
bind:value={editingSectionTitle}
class="text-lg font-semibold"
- placeholder="Section title"
+ placeholder={t('dashboard.editor.sectionTitlePlaceholder')}
onkeydown={handleSectionEditKeydown}
dataTestid="dashboard-section-title-input"
/>
{:else}
@@ -401,9 +420,9 @@
id={`dashboard-section-heading-${section.id}`}
class="text-lg font-semibold"
style="color: var(--ds-text);"
- >{section.title}
- {#if section.subtitle}
- {section.subtitle}
+ >{sectionDisplay.title}
+ {#if sectionDisplay.subtitle}
+ {sectionDisplay.subtitle}
{/if}
{#if isEditMode}
@@ -437,7 +456,8 @@
class="p-2 rounded transition-colors hover:bg-[var(--ds-background-neutral-hovered)]"
style="color: var(--ds-text-subtle);"
onclick={() => startEditingSection(section)}
- title="Rename section"
+ title={t('dashboard.editor.renameSection')}
+ aria-label={t('dashboard.editor.renameSection')}
>
@@ -446,7 +466,8 @@
class="p-2 rounded transition-colors hover:bg-[var(--ds-background-neutral-hovered)]"
style="color: var(--ds-text-subtle);"
onclick={() => handleDeleteSection(section.id)}
- title="Delete section"
+ title={t('dashboard.editor.deleteSection')}
+ aria-label={t('dashboard.editor.deleteSection')}
>
@@ -511,7 +532,7 @@
/>
{:else}
- Unknown widget type: {widget.type}
+ {t('dashboard.editor.unknownWidgetType', { type: widget.type })}
{/if}
@@ -519,8 +540,8 @@
{:else}
-
No widgets in this section yet
-
Click "Customize" to add widgets
+
{t('dashboard.editor.noWidgets')}
+
{t('dashboard.editor.addWidgetsHint')}
{/if}
@@ -530,8 +551,8 @@
{#if sections.length === 0}
-
No sections configured
-
Click "Edit" to add sections to your dashboard
+
{t('dashboard.editor.noSections')}
+
{t('dashboard.editor.addSectionsHint')}
{/if}
diff --git a/frontend/src/lib/services/dashboardWidgetRegistry.js b/frontend/src/lib/services/dashboardWidgetRegistry.js
index cab833051..ab591c25e 100644
--- a/frontend/src/lib/services/dashboardWidgetRegistry.js
+++ b/frontend/src/lib/services/dashboardWidgetRegistry.js
@@ -14,8 +14,8 @@ export const dashboardWidgetRegistry = [
// Activity & news
{
type: 'daily-briefing',
- name: 'Daily Briefing',
- description: 'AI-generated summary of what matters to you today',
+ nameKey: 'dashboard.widgetCatalog.dailyBriefing.name',
+ descriptionKey: 'dashboard.widgetCatalog.dailyBriefing.description',
category: dashboardWidgetCategories.ACTIVITY,
icon: 'Sparkles',
defaultWidth: 12,
@@ -23,8 +23,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'your-activity',
- name: 'Your Activity',
- description: 'Items you recently viewed, edited, or commented on',
+ nameKey: 'dashboard.widgetCatalog.yourActivity.name',
+ descriptionKey: 'dashboard.widgetCatalog.yourActivity.description',
category: dashboardWidgetCategories.ACTIVITY,
icon: 'Clock',
defaultWidth: 8,
@@ -32,8 +32,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'whats-new',
- name: "What's New",
- description: 'Latest notifications and unread updates',
+ nameKey: 'dashboard.widgetCatalog.whatsNew.name',
+ descriptionKey: 'dashboard.widgetCatalog.whatsNew.description',
category: dashboardWidgetCategories.ACTIVITY,
icon: 'Bell',
defaultWidth: 4,
@@ -43,8 +43,8 @@ export const dashboardWidgetRegistry = [
// Work items
{
type: 'personal-tasks',
- name: 'Personal Tasks',
- description: 'Items from your personal todo list',
+ nameKey: 'dashboard.widgetCatalog.personalTasks.name',
+ descriptionKey: 'dashboard.widgetCatalog.personalTasks.description',
category: dashboardWidgetCategories.WORK,
icon: 'ListChecks',
defaultWidth: 6,
@@ -52,8 +52,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'saved-search',
- name: 'Saved Search',
- description: 'Display work items from a saved collection',
+ nameKey: 'dashboard.widgetCatalog.savedSearch.name',
+ descriptionKey: 'dashboard.widgetCatalog.savedSearch.description',
category: dashboardWidgetCategories.WORK,
icon: 'Search',
defaultWidth: 6,
@@ -61,8 +61,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'assigned-to-me',
- name: 'Assigned to Me',
- description: 'Open items assigned to you across all workspaces',
+ nameKey: 'dashboard.widgetCatalog.assignedToMe.name',
+ descriptionKey: 'dashboard.widgetCatalog.assignedToMe.description',
category: dashboardWidgetCategories.WORK,
icon: 'CheckSquare',
defaultWidth: 6,
@@ -70,8 +70,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'watched-items',
- name: 'Watched Items',
- description: 'Items you are following',
+ nameKey: 'dashboard.widgetCatalog.watchedItems.name',
+ descriptionKey: 'dashboard.widgetCatalog.watchedItems.description',
category: dashboardWidgetCategories.WORK,
icon: 'Eye',
defaultWidth: 4,
@@ -79,8 +79,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'upcoming-milestones',
- name: 'Upcoming Milestones',
- description: 'Milestones with approaching target dates',
+ nameKey: 'dashboard.widgetCatalog.upcomingMilestones.name',
+ descriptionKey: 'dashboard.widgetCatalog.upcomingMilestones.description',
category: dashboardWidgetCategories.WORK,
icon: 'Target',
defaultWidth: 12,
@@ -90,8 +90,8 @@ export const dashboardWidgetRegistry = [
// Navigation
{
type: 'recent-workspaces',
- name: 'Recent Workspaces',
- description: 'Workspaces you recently visited',
+ nameKey: 'dashboard.widgetCatalog.recentWorkspaces.name',
+ descriptionKey: 'dashboard.widgetCatalog.recentWorkspaces.description',
category: dashboardWidgetCategories.NAVIGATION,
icon: 'Briefcase',
defaultWidth: 8,
@@ -99,8 +99,8 @@ export const dashboardWidgetRegistry = [
},
{
type: 'quick-access',
- name: 'Quick Access',
- description: 'Quick links to workspaces you can reach',
+ nameKey: 'dashboard.widgetCatalog.quickAccess.name',
+ descriptionKey: 'dashboard.widgetCatalog.quickAccess.description',
category: dashboardWidgetCategories.NAVIGATION,
icon: 'Grip',
defaultWidth: 4,
@@ -126,33 +126,73 @@ export function getDashboardWidgetMinWidth(type) {
return widget?.minWidth ?? 3;
}
+export const defaultDashboardSections = {
+ 'default-your-day': {
+ title: 'Your Day',
+ subtitle: 'A quick read on what needs your attention',
+ titleKey: 'dashboard.sections.yourDay.title',
+ subtitleKey: 'dashboard.sections.yourDay.subtitle',
+ },
+ 'default-work': {
+ title: 'Work',
+ subtitle: 'Your personal list and items assigned to you',
+ titleKey: 'dashboard.sections.work.title',
+ subtitleKey: 'dashboard.sections.work.subtitle',
+ },
+ 'default-workspaces': {
+ title: 'Workspaces',
+ subtitle: 'Jump back in',
+ titleKey: 'dashboard.sections.workspaces.title',
+ subtitleKey: 'dashboard.sections.workspaces.subtitle',
+ },
+};
+
+/**
+ * Translate untouched built-in section headings while preserving user edits.
+ */
+export function getDashboardSectionDisplay(section, translate) {
+ const defaults = defaultDashboardSections[section.id];
+ if (!defaults) return { title: section.title, subtitle: section.subtitle };
+ return {
+ title: section.title === defaults.title ? translate(defaults.titleKey) : section.title,
+ subtitle:
+ section.subtitle === defaults.subtitle ? translate(defaults.subtitleKey) : section.subtitle,
+ };
+}
+
+/**
+ * Preserve canonical default values when a localized section editor is saved
+ * without changing its translated display text.
+ */
+export function getDashboardSectionSaveValues(section, draft, translate) {
+ const display = getDashboardSectionDisplay(section, translate);
+ return {
+ title: draft.title === display.title ? section.title : draft.title,
+ subtitle: draft.subtitle === (display.subtitle || '') ? section.subtitle : draft.subtitle,
+ };
+}
+
/**
* Build the default three-section layout shown to users who have never
* customized their dashboard (or whose saved layout is empty).
*/
export function buildDefaultDashboardLayout() {
+ const section = (id, displayOrder, widgetIds) => ({
+ id,
+ title: defaultDashboardSections[id].title,
+ subtitle: defaultDashboardSections[id].subtitle,
+ display_order: displayOrder,
+ widget_ids: widgetIds,
+ });
+
const sections = [
- {
- id: 'default-your-day',
- title: 'Your Day',
- subtitle: 'A quick read on what needs your attention',
- display_order: 0,
- widget_ids: ['default-daily-briefing', 'default-your-activity', 'default-whats-new'],
- },
- {
- id: 'default-work',
- title: 'Work',
- subtitle: 'Your personal list and items assigned to you',
- display_order: 1,
- widget_ids: ['default-personal-tasks', 'default-assigned-to-me'],
- },
- {
- id: 'default-workspaces',
- title: 'Workspaces',
- subtitle: 'Jump back in',
- display_order: 2,
- widget_ids: ['default-recent-workspaces', 'default-quick-access'],
- },
+ section('default-your-day', 0, [
+ 'default-daily-briefing',
+ 'default-your-activity',
+ 'default-whats-new',
+ ]),
+ section('default-work', 1, ['default-personal-tasks', 'default-assigned-to-me']),
+ section('default-workspaces', 2, ['default-recent-workspaces', 'default-quick-access']),
];
const widget = (id, type, sectionId, position, width) => ({
diff --git a/frontend/src/lib/stores/homepageStore.svelte.js b/frontend/src/lib/stores/homepageStore.svelte.js
index 05ced8e78..761cf0b88 100644
--- a/frontend/src/lib/stores/homepageStore.svelte.js
+++ b/frontend/src/lib/stores/homepageStore.svelte.js
@@ -14,7 +14,10 @@ import {
getDashboardWidgetDefaultWidth,
getDashboardWidgetMinWidth,
} from '../services/dashboardWidgetRegistry.js';
-import { formatDateSimple, formatDateWithOptions } from '../utils/dateFormatter.js';
+import {
+ formatDateWithOptions,
+ formatRelativeTime as formatRelativeTimeValue,
+} from '../utils/dateFormatter.js';
import { t } from './i18n.svelte.js';
import { errorToast } from './toasts.svelte.js';
@@ -187,7 +190,7 @@ class HomepageStore {
// === Section management ===
- addSection(title = 'New Section', subtitle = '') {
+ addSection(title = t('dashboard.editor.newSection'), subtitle = '') {
const newSection = {
id: crypto.randomUUID(),
title,
@@ -393,13 +396,13 @@ class HomepageStore {
// Determine greeting based on time of day
if (hour >= 5 && hour < 12) {
- this.greeting = 'Good morning';
+ this.greeting = 'dashboard.goodMorning';
} else if (hour >= 12 && hour < 18) {
- this.greeting = 'Good afternoon';
+ this.greeting = 'dashboard.goodAfternoon';
} else if (hour >= 18 && hour < 22) {
- this.greeting = 'Good evening';
+ this.greeting = 'dashboard.goodEvening';
} else {
- this.greeting = 'Good night';
+ this.greeting = 'dashboard.goodNight';
}
// Format current date
@@ -447,21 +450,7 @@ class HomepageStore {
* Format relative time.
*/
formatRelativeTime(timestamp) {
- if (!timestamp) return 'Unknown';
-
- const now = new Date();
- const then = new Date(timestamp);
- const diffMs = now.getTime() - then.getTime();
- const diffMins = Math.floor(diffMs / 60000);
- const diffHours = Math.floor(diffMs / 3600000);
- const diffDays = Math.floor(diffMs / 86400000);
-
- if (diffMins < 1) return 'Just now';
- if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
- if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
- if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
-
- return formatDateSimple(then);
+ return timestamp ? formatRelativeTimeValue(timestamp) : t('common.unknown');
}
/**
diff --git a/frontend/src/lib/stores/i18n-utils.spec.js b/frontend/src/lib/stores/i18n-utils.spec.js
new file mode 100644
index 000000000..d5b759837
--- /dev/null
+++ b/frontend/src/lib/stores/i18n-utils.spec.js
@@ -0,0 +1,131 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+ buildDefaultDashboardLayout,
+ getDashboardSectionDisplay,
+ getDashboardSectionSaveValues,
+} from '../services/dashboardWidgetRegistry.js';
+import { formatRelativeTime } from '../utils/dateFormatter.js';
+import { i18n } from './i18n.svelte.js';
+
+describe('dashboard section localization', () => {
+ it('translates untouched default sections', () => {
+ const [section] = buildDefaultDashboardLayout().sections;
+ const translate = (key) => `translated:${key}`;
+
+ expect(getDashboardSectionDisplay(section, translate)).toEqual({
+ title: 'translated:dashboard.sections.yourDay.title',
+ subtitle: 'translated:dashboard.sections.yourDay.subtitle',
+ });
+ });
+
+ it('preserves customized section text', () => {
+ const section = {
+ ...buildDefaultDashboardLayout().sections[0],
+ title: 'My focus',
+ subtitle: 'What matters now',
+ };
+
+ expect(getDashboardSectionDisplay(section, () => 'translated')).toEqual({
+ title: 'My focus',
+ subtitle: 'What matters now',
+ });
+ });
+
+ it('does not persist localized default text when an unchanged section is saved', () => {
+ const [section] = buildDefaultDashboardLayout().sections;
+ const german = (key) =>
+ ({
+ 'dashboard.sections.yourDay.title': 'Dein Tag',
+ 'dashboard.sections.yourDay.subtitle':
+ 'Ein kurzer Blick auf alles, was Ihre Aufmerksamkeit braucht',
+ })[key];
+ const english = (key) =>
+ ({
+ 'dashboard.sections.yourDay.title': 'Your Day',
+ 'dashboard.sections.yourDay.subtitle': 'A quick read on what needs your attention',
+ })[key];
+ const displayedInGerman = getDashboardSectionDisplay(section, german);
+
+ const savedValues = getDashboardSectionSaveValues(section, displayedInGerman, german);
+ const savedSection = { ...section, ...savedValues };
+
+ expect(savedValues).toEqual({
+ title: 'Your Day',
+ subtitle: 'A quick read on what needs your attention',
+ });
+ expect(getDashboardSectionDisplay(savedSection, english)).toEqual({
+ title: 'Your Day',
+ subtitle: 'A quick read on what needs your attention',
+ });
+ expect(getDashboardSectionDisplay(savedSection, german)).toEqual(displayedInGerman);
+ });
+
+ it('persists actual edits made to localized section text', () => {
+ const [section] = buildDefaultDashboardLayout().sections;
+ const translate = (key) => `translated:${key}`;
+
+ expect(
+ getDashboardSectionSaveValues(
+ section,
+ { title: 'Mein Fokus', subtitle: 'Heute wichtig' },
+ translate
+ )
+ ).toEqual({
+ title: 'Mein Fokus',
+ subtitle: 'Heute wichtig',
+ });
+ });
+
+ it('preserves edits for custom sections', () => {
+ const section = {
+ id: 'custom-section',
+ title: 'Eigener Bereich',
+ subtitle: 'Meine Übersicht',
+ };
+
+ expect(
+ getDashboardSectionSaveValues(
+ section,
+ { title: 'Neuer Bereich', subtitle: 'Neue Übersicht' },
+ () => 'translated'
+ )
+ ).toEqual({
+ title: 'Neuer Bereich',
+ subtitle: 'Neue Übersicht',
+ });
+ });
+});
+
+describe('locale-aware relative time', () => {
+ afterEach(async () => {
+ vi.useRealTimers();
+ await i18n.setLocale('en');
+ });
+
+ it('uses the active application locale', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-08-28T12:00:00Z'));
+ await i18n.setLocale('de');
+
+ expect(formatRelativeTime('2026-08-28T11:39:00Z')).toBe('vor 21 Minuten');
+ });
+
+ it('keeps the English fallback behavior', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-08-28T12:00:00Z'));
+ await i18n.setLocale('en');
+
+ expect(formatRelativeTime('2026-08-28T11:39:00Z')).toBe('21 minutes ago');
+ });
+
+ it('describes elapsed weeks and months instead of calendar periods', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-08-28T12:00:00Z'));
+ await i18n.setLocale('en');
+
+ expect(formatRelativeTime('2026-08-20T12:00:00Z')).toBe('1 week ago');
+ expect(formatRelativeTime('2026-07-14T12:00:00Z')).toBe('1 month ago');
+ expect(formatRelativeTime('2026-08-27T02:00:00Z')).toBe('1 day ago');
+ expect(formatRelativeTime('2026-09-05T12:00:00Z')).toBe('in 1 week');
+ });
+});
diff --git a/frontend/src/lib/utils/dateFormatter.js b/frontend/src/lib/utils/dateFormatter.js
index 2141c48eb..dc2554a58 100644
--- a/frontend/src/lib/utils/dateFormatter.js
+++ b/frontend/src/lib/utils/dateFormatter.js
@@ -261,27 +261,21 @@ export function formatRelativeTime(dateString) {
if (!dateString) return '';
try {
const date = new Date(dateString);
+ if (Number.isNaN(date.getTime())) return '';
const now = serverNow();
- const diffMs = now.getTime() - date.getTime();
- const diffSecs = Math.floor(diffMs / 1000);
- const diffMins = Math.floor(diffSecs / 60);
- const diffHours = Math.floor(diffMins / 60);
- const diffDays = Math.floor(diffHours / 24);
-
- if (diffSecs < 60) return 'just now';
- if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
- if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
- if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
- if (diffDays < 30) {
- const weeks = Math.floor(diffDays / 7);
- return `${weeks} week${weeks !== 1 ? 's' : ''} ago`;
- }
- if (diffDays < 365) {
- const months = Math.floor(diffDays / 30);
- return `${months} month${months !== 1 ? 's' : ''} ago`;
- }
- const years = Math.floor(diffDays / 365);
- return `${years} year${years !== 1 ? 's' : ''} ago`;
+ const diffMs = date.getTime() - now.getTime();
+ const absoluteMs = Math.abs(diffMs);
+ const formatter = new Intl.RelativeTimeFormat(getAppLocale(), { numeric: 'always' });
+ const immediateFormatter = new Intl.RelativeTimeFormat(getAppLocale(), { numeric: 'auto' });
+ const valueFor = (unitMs) => Math.sign(diffMs) * Math.floor(absoluteMs / unitMs);
+
+ if (absoluteMs < 60_000) return immediateFormatter.format(0, 'second');
+ if (absoluteMs < 3_600_000) return formatter.format(valueFor(60_000), 'minute');
+ if (absoluteMs < 86_400_000) return formatter.format(valueFor(3_600_000), 'hour');
+ if (absoluteMs < 604_800_000) return formatter.format(valueFor(86_400_000), 'day');
+ if (absoluteMs < 2_592_000_000) return formatter.format(valueFor(604_800_000), 'week');
+ if (absoluteMs < 31_536_000_000) return formatter.format(valueFor(2_592_000_000), 'month');
+ return formatter.format(valueFor(31_536_000_000), 'year');
} catch (error) {
console.error('Error formatting relative time:', error);
return '';
diff --git a/frontend/src/lib/widgets/dashboard/AssignedToMeWidget.svelte b/frontend/src/lib/widgets/dashboard/AssignedToMeWidget.svelte
index fe480e95d..625886c7f 100644
--- a/frontend/src/lib/widgets/dashboard/AssignedToMeWidget.svelte
+++ b/frontend/src/lib/widgets/dashboard/AssignedToMeWidget.svelte
@@ -1,6 +1,7 @@