@hed-hog/lms 0.0.328 → 0.0.330

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/instructor/instructor.service.d.ts.map +1 -1
  2. package/dist/instructor/instructor.service.js +22 -16
  3. package/dist/instructor/instructor.service.js.map +1 -1
  4. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
  5. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +7 -5
  6. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
  7. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
  8. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
  9. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
  10. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
  11. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
  12. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +10 -10
  13. package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
  14. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +5 -3
  15. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
  16. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +9 -7
  17. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +3 -1
  18. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +4 -2
  19. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +24 -23
  20. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
  21. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +7 -5
  22. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
  23. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
  24. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
  25. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
  26. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
  27. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
  28. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
  29. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
  30. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
  31. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
  32. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
  33. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
  34. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
  35. package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
  36. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
  37. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +92 -26
  38. package/hedhog/frontend/app/instructors/page.tsx.ejs +4 -2
  39. package/hedhog/frontend/messages/en.json +619 -13
  40. package/hedhog/frontend/messages/pt.json +619 -13
  41. package/package.json +7 -7
  42. package/src/instructor/instructor.service.ts +22 -19
  43. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +0 -591
  44. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +0 -109
  45. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +0 -60
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +0 -134
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +0 -113
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +0 -314
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +0 -174
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +0 -185
  51. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +0 -277
  52. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +0 -207
@@ -1522,7 +1522,7 @@ export default function TurmasPage() {
1522
1522
  setCourseSheetOpen(false);
1523
1523
  toast.success(courseSheetT('toasts.courseCreated'));
1524
1524
  } catch {
1525
- toast.error('Nao foi possivel cadastrar o curso.');
1525
+ toast.error(t('messages.classCreateCourseError'));
1526
1526
  } finally {
1527
1527
  setSavingCourse(false);
1528
1528
  }
@@ -1606,7 +1606,7 @@ export default function TurmasPage() {
1606
1606
  notifyLmsDashboardUpdated();
1607
1607
  setSheetOpen(false);
1608
1608
  } catch {
1609
- toast.error('Nao foi possivel salvar a turma.');
1609
+ toast.error(t('messages.classSaveError'));
1610
1610
  } finally {
1611
1611
  setSaving(false);
1612
1612
  }
@@ -1627,7 +1627,7 @@ export default function TurmasPage() {
1627
1627
  setTurmaToDelete(null);
1628
1628
  setDeleteDialogOpen(false);
1629
1629
  } catch {
1630
- toast.error('Nao foi possivel excluir a turma.');
1630
+ toast.error(t('messages.classDeleteError'));
1631
1631
  }
1632
1632
  }
1633
1633
 
@@ -2321,9 +2321,15 @@ export default function TurmasPage() {
2321
2321
  entityLabel={t('form.fields.course.label')}
2322
2322
  initialSelectedLabel={selectedCourseTitle}
2323
2323
  searchPlaceholder={t('form.fields.course.placeholder')}
2324
- emptyStateDescription="Nenhum curso encontrado."
2325
- loadingLabel="Carregando cursos..."
2326
- noResultsLabel="Nenhum curso encontrado."
2324
+ emptyStateDescription={t(
2325
+ 'components.entityPicker.courses.empty'
2326
+ )}
2327
+ loadingLabel={t(
2328
+ 'components.entityPicker.courses.loading'
2329
+ )}
2330
+ noResultsLabel={t(
2331
+ 'components.entityPicker.courses.empty'
2332
+ )}
2327
2333
  showCreateButton={false}
2328
2334
  renderOption={({ option }) => (
2329
2335
  <div className="flex items-center gap-2 py-0.5">
@@ -2398,7 +2404,7 @@ export default function TurmasPage() {
2398
2404
  size="icon"
2399
2405
  className="shrink-0"
2400
2406
  onClick={openCourseCreateSheet}
2401
- aria-label="Cadastrar novo curso"
2407
+ aria-label={courseSheetT('actions.createCourse')}
2402
2408
  >
2403
2409
  <Plus className="h-4 w-4" />
2404
2410
  </Button>
@@ -2833,8 +2839,10 @@ export default function TurmasPage() {
2833
2839
  placeholder={t('form.fields.professor.placeholder')}
2834
2840
  initialSelectedLabel={watchedFormValues.professor ?? ''}
2835
2841
  searchPlaceholder={t('form.fields.professor.placeholder')}
2836
- emptyStateDescription="Nenhum professor encontrado."
2837
- noResultsLabel="Nenhum professor encontrado."
2842
+ emptyStateDescription={t(
2843
+ 'form.fields.professor.emptyState'
2844
+ )}
2845
+ noResultsLabel={t('form.fields.professor.noResults')}
2838
2846
  showCreateButton={false}
2839
2847
  clearable={false}
2840
2848
  getOptionValue={(opt) => opt.id}
@@ -2952,7 +2960,7 @@ export default function TurmasPage() {
2952
2960
  size="icon"
2953
2961
  className="shrink-0"
2954
2962
  onClick={() => setCreateProfessorDialogOpen(true)}
2955
- aria-label="Cadastrar novo professor"
2963
+ aria-label={t('sheet.lessonForm.createInstructor')}
2956
2964
  >
2957
2965
  <Plus className="h-4 w-4" />
2958
2966
  </Button>
@@ -2991,11 +2999,11 @@ export default function TurmasPage() {
2991
2999
  open={createProfessorDialogOpen}
2992
3000
  onOpenChange={setCreateProfessorDialogOpen}
2993
3001
  onCreated={handleProfessorCreated}
2994
- title="Cadastrar novo professor"
2995
- description="Crie um novo professor para seleciona-lo nesta turma."
2996
- submitLabel="Cadastrar"
2997
- successMessage="Professor cadastrado com sucesso."
2998
- errorMessage="Nao foi possivel cadastrar o professor."
3002
+ title={t('sheet.lessonForm.createInstructorTitle')}
3003
+ description={t('sheet.lessonForm.createInstructorDescription')}
3004
+ submitLabel={t('sheet.lessonForm.createInstructorSubmit')}
3005
+ successMessage={t('sheet.lessonForm.createInstructorSuccess')}
3006
+ errorMessage={t('sheet.lessonForm.createInstructorError')}
2999
3007
  defaultQualificationSlugs={['class-sessions']}
3000
3008
  />
3001
3009
 
@@ -27,6 +27,7 @@ import {
27
27
  RefreshCw,
28
28
  Video,
29
29
  } from 'lucide-react';
30
+ import { useTranslations } from 'next-intl';
30
31
  import { useRouter } from 'next/navigation';
31
32
 
32
33
  import { ConfirmDialog } from './structure/_components/confirm-dialog';
@@ -66,6 +67,7 @@ type ApiCourseSummary = {
66
67
  };
67
68
 
68
69
  export default function CourseStructurePage({ params }: Props) {
70
+ const t = useTranslations('lms.CoursesPage.StructurePage');
69
71
  const { id } = use(params);
70
72
  const isMobile = useIsMobile();
71
73
  const router = useRouter();
@@ -349,10 +351,10 @@ export default function CourseStructurePage({ params }: Props) {
349
351
  variant="outline"
350
352
  onClick={() => setMobileSheetOpen(true)}
351
353
  className="gap-2"
352
- aria-label="Abrir estrutura do curso"
354
+ aria-label={t('mobile.openStructure')}
353
355
  >
354
356
  <Menu className="size-4" />
355
- Estrutura
357
+ {t('breadcrumbs.structure')}
356
358
  </Button>
357
359
  </div>
358
360
 
@@ -367,7 +369,7 @@ export default function CourseStructurePage({ params }: Props) {
367
369
  <SheetContent side="left" className="w-[320px] p-0 flex flex-col">
368
370
  <SheetHeader className="px-4 py-3 border-b shrink-0">
369
371
  <SheetTitle className="text-sm">
370
- {course.title} — Estrutura
372
+ {t('mobile.sheetTitle', { title: course.title })}
371
373
  </SheetTitle>
372
374
  </SheetHeader>
373
375
  <div className="flex-1 min-h-0 overflow-hidden">
@@ -10,9 +10,11 @@ import {
10
10
  AlertDialogHeader,
11
11
  AlertDialogTitle,
12
12
  } from '@/components/ui/alert-dialog';
13
+ import { useTranslations } from 'next-intl';
13
14
  import { useStructureStore } from './store';
14
15
 
15
16
  export function ConfirmDialog() {
17
+ const t = useTranslations('lms.CoursesPage.StructurePage.confirmDialog');
16
18
  const confirmDialog = useStructureStore((s) => s.confirmDialog);
17
19
  const closeConfirm = useStructureStore((s) => s.closeConfirm);
18
20
 
@@ -31,15 +33,15 @@ export function ConfirmDialog() {
31
33
  )}
32
34
  </AlertDialogHeader>
33
35
  <AlertDialogFooter>
34
- <AlertDialogCancel onClick={closeConfirm}>Cancelar</AlertDialogCancel>
36
+ <AlertDialogCancel onClick={closeConfirm}>{t('cancel')}</AlertDialogCancel>
35
37
  <AlertDialogAction
36
38
  onClick={handleConfirm}
37
39
  className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
38
40
  >
39
- Excluir
41
+ {t('confirm')}
40
42
  </AlertDialogAction>
41
43
  </AlertDialogFooter>
42
44
  </AlertDialogContent>
43
45
  </AlertDialog>
44
46
  );
45
- }
47
+ }
@@ -3,6 +3,7 @@
3
3
  import { Button } from '@/components/ui/button';
4
4
  import { cn } from '@/lib/utils';
5
5
  import { ChevronsDownUp, ChevronsUpDown, Loader2, Plus } from 'lucide-react';
6
+ import { useTranslations } from 'next-intl';
6
7
  import { forwardRef, useMemo } from 'react';
7
8
  import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
8
9
  import { CourseTreeDnd } from './course-tree-dnd';
@@ -12,6 +13,7 @@ import { useStructureStore } from './store';
12
13
  import { buildVisibleItems } from './tree-helpers';
13
14
 
14
15
  export const CourseTreePanel = forwardRef<SearchFilterHandle>((_, ref) => {
16
+ const t = useTranslations('lms.CoursesPage.StructurePage');
15
17
  const course = useStructureStore((s) => s.course);
16
18
  const sessions = useStructureStore((s) => s.sessions);
17
19
  const lessons = useStructureStore((s) => s.lessons);
@@ -57,10 +59,10 @@ export const CourseTreePanel = forwardRef<SearchFilterHandle>((_, ref) => {
57
59
  )}
58
60
  title={
59
61
  allExpanded
60
- ? 'Recolher tudo (Ctrl+Shift+E)'
61
- : 'Expandir tudo (Ctrl+Shift+E)'
62
+ ? t('tree.collapseAllShortcut')
63
+ : t('tree.expandAllShortcut')
62
64
  }
63
- aria-label={allExpanded ? 'Recolher tudo' : 'Expandir tudo'}
65
+ aria-label={allExpanded ? t('tree.collapseAll') : t('tree.expandAll')}
64
66
  onClick={allExpanded ? collapseAll : expandAll}
65
67
  disabled={isFiltering}
66
68
  >
@@ -75,8 +77,8 @@ export const CourseTreePanel = forwardRef<SearchFilterHandle>((_, ref) => {
75
77
  variant="ghost"
76
78
  size="icon"
77
79
  className="size-8 shrink-0"
78
- title="Nova sessão"
79
- aria-label="Nova sessão"
80
+ title={t('tree.addSession')}
81
+ aria-label={t('tree.addSession')}
80
82
  disabled={createSession.isPending}
81
83
  onClick={() => createSession.mutate()}
82
84
  >
@@ -92,8 +94,8 @@ export const CourseTreePanel = forwardRef<SearchFilterHandle>((_, ref) => {
92
94
  {isFiltering && resultCount !== undefined && (
93
95
  <div className="px-3 py-1 text-[0.65rem] text-muted-foreground bg-muted/30 border-b shrink-0">
94
96
  {resultCount === 0
95
- ? 'Nenhum resultado encontrado'
96
- : `${resultCount} resultado${resultCount === 1 ? '' : 's'} encontrado${resultCount === 1 ? '' : 's'}`}
97
+ ? t('search.noResults')
98
+ : t('search.results', { count: resultCount })}
97
99
  </div>
98
100
  )}
99
101
 
@@ -1,4 +1,5 @@
1
1
  import { Skeleton } from '@/components/ui/skeleton';
2
+ import { useTranslations } from 'next-intl';
2
3
 
3
4
  /**
4
5
  * CourseTreeSkeleton
@@ -7,11 +8,12 @@ import { Skeleton } from '@/components/ui/skeleton';
7
8
  * the API. Mimics the visual shape of session headers and lesson rows.
8
9
  */
9
10
  export function CourseTreeSkeleton() {
11
+ const t = useTranslations('lms.CoursesPage.StructurePage');
10
12
  return (
11
13
  <div
12
14
  className="flex flex-col gap-1 p-3"
13
15
  aria-busy="true"
14
- aria-label="Carregando estrutura do curso"
16
+ aria-label={t('loading')}
15
17
  >
16
18
  {/* Course header row */}
17
19
  <div className="flex items-center gap-2 px-2 py-1.5">
@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils';
4
4
  import type { DraggableAttributes } from '@dnd-kit/core';
5
5
  import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
6
6
  import { GripVertical } from 'lucide-react';
7
+ import { useTranslations } from 'next-intl';
7
8
 
8
9
  interface DragHandleProps {
9
10
  listeners?: SyntheticListenerMap;
@@ -23,6 +24,7 @@ export function DragHandle({
23
24
  disabled,
24
25
  className,
25
26
  }: DragHandleProps) {
27
+ const t = useTranslations('lms.CoursesPage.StructurePage.dragHandle');
26
28
  if (disabled) {
27
29
  return (
28
30
  <span
@@ -30,7 +32,7 @@ export function DragHandle({
30
32
  'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
31
33
  className
32
34
  )}
33
- title="Limpe a busca para reordenar"
35
+ title={t('disabled')}
34
36
  >
35
37
  <GripVertical className="size-3.5" />
36
38
  </span>
@@ -48,7 +50,7 @@ export function DragHandle({
48
50
  'transition-colors touch-none',
49
51
  className
50
52
  )}
51
- title="Arrastar para reordenar"
53
+ title={t('enabled')}
52
54
  // Prevent click events from bubbling to the row selection handler
53
55
  onClick={(e) => e.stopPropagation()}
54
56
  >
@@ -10,6 +10,7 @@ import {
10
10
  Video,
11
11
  X,
12
12
  } from 'lucide-react';
13
+ import { useTranslations } from 'next-intl';
13
14
  import { useState } from 'react';
14
15
  import { toast } from 'sonner';
15
16
 
@@ -43,6 +44,7 @@ const STATUS_LABELS: Record<LessonStatus, string> = {
43
44
  // ── Component ─────────────────────────────────────────────────────────────────
44
45
 
45
46
  export function EditorBulk() {
47
+ const t = useTranslations('lms.CoursesPage.StructurePage.bulkEditor');
46
48
  const selectedIds = useStructureStore((s) => s.selectedIds);
47
49
  const sessions = useStructureStore((s) => s.sessions);
48
50
  const lessons = useStructureStore((s) => s.lessons);
@@ -74,8 +76,8 @@ export function EditorBulk() {
74
76
  );
75
77
  toast.success(
76
78
  lines.length
77
- ? `Edição em massa: ${lines.join(', ')} (mock)`
78
- : 'Nenhuma alteração selecionada'
79
+ ? t('toast.preview', { changes: lines.join(', ') })
80
+ : t('toast.none')
79
81
  );
80
82
  }
81
83
 
@@ -93,11 +95,11 @@ export function EditorBulk() {
93
95
  )}
94
96
  </div>
95
97
  <div className="flex-1 min-w-0">
96
- <p className="text-sm font-semibold">Edição em massa</p>
98
+ <p className="text-sm font-semibold">{t('title')}</p>
97
99
  <p className="text-[0.65rem] text-muted-foreground">
98
100
  {selectedIds.size}{' '}
99
- {allLessons ? 'aulas' : allSessions ? 'sessões' : 'itens'}{' '}
100
- selecionados
101
+ {allLessons ? t('types.lessons') : allSessions ? t('types.sessions') : t('types.items')}{' '}
102
+ {t('types.selected')}
101
103
  </p>
102
104
  </div>
103
105
  <Button
@@ -105,7 +107,7 @@ export function EditorBulk() {
105
107
  size="icon"
106
108
  className="size-7 shrink-0"
107
109
  onClick={clearSelection}
108
- title="Limpar seleção"
110
+ title={t('clearSelection')}
109
111
  >
110
112
  <X className="size-3.5" />
111
113
  </Button>
@@ -148,7 +150,7 @@ export function EditorBulk() {
148
150
  <Card>
149
151
  <CardHeader className="px-3 pt-3 pb-2">
150
152
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
151
- Status de produção
153
+ {t('productionStatus')}
152
154
  </CardTitle>
153
155
  </CardHeader>
154
156
  <CardContent className="px-3 pb-3">
@@ -157,14 +159,14 @@ export function EditorBulk() {
157
159
  onValueChange={(v) => setStatus(v as LessonStatus)}
158
160
  >
159
161
  <SelectTrigger className="h-8 text-xs w-full">
160
- <SelectValue placeholder="Manter atual" />
162
+ <SelectValue placeholder={t('keepCurrent')} />
161
163
  </SelectTrigger>
162
164
  <SelectContent>
163
- <SelectItem value="preparada">Preparada</SelectItem>
164
- <SelectItem value="gravada">Gravada</SelectItem>
165
- <SelectItem value="editada">Editada</SelectItem>
166
- <SelectItem value="finalizada">Finalizada</SelectItem>
167
- <SelectItem value="publicada">Publicada</SelectItem>
165
+ <SelectItem value="preparada">{t('status.preparada')}</SelectItem>
166
+ <SelectItem value="gravada">{t('status.gravada')}</SelectItem>
167
+ <SelectItem value="editada">{t('status.editada')}</SelectItem>
168
+ <SelectItem value="finalizada">{t('status.finalizada')}</SelectItem>
169
+ <SelectItem value="publicada">{t('status.publicada')}</SelectItem>
168
170
  </SelectContent>
169
171
  </Select>
170
172
  </CardContent>
@@ -175,7 +177,7 @@ export function EditorBulk() {
175
177
  <Card>
176
178
  <CardHeader className="px-3 pt-3 pb-2">
177
179
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
178
- Visibilidade
180
+ {t('visibilityTitle')}
179
181
  </CardTitle>
180
182
  </CardHeader>
181
183
  <CardContent className="px-3 pb-3">
@@ -184,22 +186,22 @@ export function EditorBulk() {
184
186
  onValueChange={(v) => setVisibility(v as Visibility)}
185
187
  >
186
188
  <SelectTrigger className="h-8 text-xs w-full">
187
- <SelectValue placeholder="Manter atual" />
189
+ <SelectValue placeholder={t('keepCurrent')} />
188
190
  </SelectTrigger>
189
191
  <SelectContent>
190
192
  <SelectItem value="publico">
191
193
  <span className="flex items-center gap-1.5">
192
- <Eye className="size-3" /> Público
194
+ <Eye className="size-3" /> {t('visibility.publico')}
193
195
  </span>
194
196
  </SelectItem>
195
197
  <SelectItem value="privado">
196
198
  <span className="flex items-center gap-1.5">
197
- <EyeOff className="size-3" /> Privado
199
+ <EyeOff className="size-3" /> {t('visibility.privado')}
198
200
  </span>
199
201
  </SelectItem>
200
202
  <SelectItem value="restrito">
201
203
  <span className="flex items-center gap-1.5">
202
- <Lock className="size-3" /> Restrito
204
+ <Lock className="size-3" /> {t('visibility.restrito')}
203
205
  </span>
204
206
  </SelectItem>
205
207
  </SelectContent>
@@ -213,13 +215,13 @@ export function EditorBulk() {
213
215
  <CardHeader className="px-3 pt-3 pb-2">
214
216
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
215
217
  <FolderInput className="size-3" />
216
- Mover para sessão
218
+ {t('moveToSession')}
217
219
  </CardTitle>
218
220
  </CardHeader>
219
221
  <CardContent className="px-3 pb-3">
220
222
  <Select value={targetSession} onValueChange={setTargetSession}>
221
223
  <SelectTrigger className="h-8 text-xs w-full">
222
- <SelectValue placeholder="Selecionar sessão…" />
224
+ <SelectValue placeholder={t('selectSession')} />
223
225
  </SelectTrigger>
224
226
  <SelectContent>
225
227
  {sessions.map((s) => (
@@ -238,8 +240,7 @@ export function EditorBulk() {
238
240
 
239
241
  <div className="rounded-md border border-amber-200 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2">
240
242
  <p className="text-[0.65rem] text-amber-700 dark:text-amber-400">
241
- As alterações em massa ainda não estão integradas com a API.
242
- Clique em salvar para ver a pré-visualização.
243
+ {t('apiNotice')}
243
244
  </p>
244
245
  </div>
245
246
  </div>
@@ -257,7 +258,7 @@ export function EditorBulk() {
257
258
  onClick={clearSelection}
258
259
  >
259
260
  <X className="size-3 mr-1" />
260
- Limpar seleção
261
+ {t('clearSelection')}
261
262
  </Button>
262
263
  <div className="flex-1" />
263
264
  <Button
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
4
4
  import { Separator } from '@/components/ui/separator';
5
5
  import { cn } from '@/lib/utils';
6
6
  import { Copy, FolderOpen, Loader2, Trash2, X } from 'lucide-react';
7
+ import { useTranslations } from 'next-intl';
7
8
  import { toast } from 'sonner';
8
9
  import {
9
10
  useBulkDeleteMutation,
@@ -18,6 +19,7 @@ import { useStructureStore } from './store';
18
19
  * Fixed below the search toolbar inside CourseTreePanel.
19
20
  */
20
21
  export function MultiSelectBar() {
22
+ const t = useTranslations('lms.CoursesPage.StructurePage.multiSelectBar');
21
23
  const selectedIds = useStructureStore((s) => s.selectedIds);
22
24
  const lessons = useStructureStore((s) => s.lessons);
23
25
  const sessions = useStructureStore((s) => s.sessions);
@@ -59,12 +61,12 @@ export function MultiSelectBar() {
59
61
  function handleCopy() {
60
62
  if (allAreLessons) {
61
63
  copyItems(selectedLessonIds, 'lesson');
62
- toast.success(`${count} aulas copiadas`);
64
+ toast.success(t('copyLessonsSuccess', { count }));
63
65
  } else if (allAreSessions) {
64
66
  copyItems(selectedSessionIds, 'session');
65
- toast.success(`${count} sessões copiadas`);
67
+ toast.success(t('copySessionsSuccess', { count }));
66
68
  } else {
67
- toast('Selecione apenas aulas ou apenas sessões para copiar.');
69
+ toast(t('copyMixedError'));
68
70
  }
69
71
  }
70
72
 
@@ -82,7 +84,7 @@ export function MultiSelectBar() {
82
84
  );
83
85
  };
84
86
  duplicateNext(0);
85
- toast.success(`${count} aulas sendo duplicadas...`);
87
+ toast.success(t('duplicatingLessons', { count }));
86
88
  } else if (allAreSessions) {
87
89
  const duplicateNext = (index: number) => {
88
90
  if (index >= selectedSessionIds.length) return;
@@ -92,7 +94,7 @@ export function MultiSelectBar() {
92
94
  );
93
95
  };
94
96
  duplicateNext(0);
95
- toast.success(`${count} sessões sendo duplicadas...`);
97
+ toast.success(t('duplicatingSessions', { count }));
96
98
  }
97
99
  }
98
100
 
@@ -139,10 +141,10 @@ export function MultiSelectBar() {
139
141
  .map((l) => ({ lessonId: l.id, sessionId: l.sessionId }));
140
142
 
141
143
  showConfirm({
142
- title: `Excluir ${label}?`,
144
+ title: t('deleteTitle', { label }),
143
145
  description: allAreSessions
144
- ? 'As sessões e todas as suas aulas serão excluídas permanentemente.'
145
- : 'Esta ação não pode ser desfeita.',
146
+ ? t('deleteSessionsDescription')
147
+ : t('deleteDescription'),
146
148
  onConfirm: () =>
147
149
  bulkDelete.mutate({
148
150
  sessionIds: selectedSessionIds,
@@ -154,7 +156,7 @@ export function MultiSelectBar() {
154
156
  return (
155
157
  <div
156
158
  role="toolbar"
157
- aria-label={`${count} itens selecionados — ações`}
159
+ aria-label={t('toolbarAria', { count })}
158
160
  className={cn(
159
161
  'flex items-center gap-0.5 px-2 py-1 shrink-0 border-b',
160
162
  'bg-primary/5 dark:bg-primary/10'
@@ -177,8 +179,8 @@ export function MultiSelectBar() {
177
179
  variant="ghost"
178
180
  size="icon"
179
181
  className="size-6 text-muted-foreground hover:text-foreground"
180
- title="Copiar selecionados (Ctrl+C)"
181
- aria-label="Copiar selecionados"
182
+ title={t('copyTitle')}
183
+ aria-label={t('copyAria')}
182
184
  onClick={handleCopy}
183
185
  >
184
186
  <Copy className="size-3" />
@@ -189,8 +191,8 @@ export function MultiSelectBar() {
189
191
  variant="ghost"
190
192
  size="icon"
191
193
  className="size-6 text-muted-foreground hover:text-foreground"
192
- title="Duplicar selecionados (Ctrl+D)"
193
- aria-label="Duplicar selecionados"
194
+ title={t('duplicateTitle')}
195
+ aria-label={t('duplicateAria')}
194
196
  disabled={isDuplicating}
195
197
  onClick={handleDuplicate}
196
198
  >
@@ -216,8 +218,8 @@ export function MultiSelectBar() {
216
218
  variant="ghost"
217
219
  size="icon"
218
220
  className="size-6 text-muted-foreground hover:text-foreground"
219
- title="Mover para outra sessão"
220
- aria-label="Mover para outra sessão"
221
+ title={t('moveTitle')}
222
+ aria-label={t('moveAria')}
221
223
  disabled={isMoving}
222
224
  onClick={handleMove}
223
225
  >
@@ -234,8 +236,8 @@ export function MultiSelectBar() {
234
236
  variant="ghost"
235
237
  size="icon"
236
238
  className="size-6 text-destructive/60 hover:text-destructive"
237
- title="Excluir selecionados (Delete)"
238
- aria-label="Excluir selecionados"
239
+ title={t('deleteActionTitle')}
240
+ aria-label={t('deleteActionAria')}
239
241
  disabled={bulkDelete.isPending}
240
242
  onClick={handleDelete}
241
243
  >
@@ -253,8 +255,8 @@ export function MultiSelectBar() {
253
255
  variant="ghost"
254
256
  size="icon"
255
257
  className="size-6 text-muted-foreground hover:text-foreground"
256
- title="Limpar seleção (Escape)"
257
- aria-label="Limpar seleção"
258
+ title={t('clearTitle')}
259
+ aria-label={t('clearAria')}
258
260
  onClick={clearSelection}
259
261
  >
260
262
  <X className="size-3" />
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Keyboard } from 'lucide-react';
4
+ import { useTranslations } from 'next-intl';
4
5
 
5
6
  import { Badge } from '@/components/ui/badge';
6
7
  import { Button } from '@/components/ui/button';
@@ -56,13 +57,14 @@ interface ShortcutsHelpProps {
56
57
  }
57
58
 
58
59
  export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
60
+ const t = useTranslations('lms.CoursesPage.StructurePage.shortcuts');
59
61
  return (
60
62
  <Dialog open={open} onOpenChange={onOpenChange}>
61
63
  <DialogContent className="max-w-sm">
62
64
  <DialogHeader>
63
65
  <DialogTitle className="flex items-center gap-2">
64
66
  <Keyboard className="size-4" />
65
- Atalhos de teclado
67
+ {t('title')}
66
68
  </DialogTitle>
67
69
  </DialogHeader>
68
70
 
@@ -103,8 +105,7 @@ export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
103
105
  </div>
104
106
 
105
107
  <p className="text-[0.65rem] text-muted-foreground mt-1">
106
- Atalhos são desativados quando o foco está em inputs, exceto Ctrl+S,
107
- Ctrl+F e Ctrl+/ que funcionam em qualquer contexto.
108
+ {t('footer')}
108
109
  </p>
109
110
  </DialogContent>
110
111
  </Dialog>
@@ -115,16 +116,17 @@ export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
115
116
  * Convenience trigger button — renders standalone when no external state is needed.
116
117
  */
117
118
  export function ShortcutsHelpTrigger({ onOpen }: { onOpen: () => void }) {
119
+ const t = useTranslations('lms.CoursesPage.StructurePage.shortcuts');
118
120
  return (
119
121
  <Button
120
122
  variant="ghost"
121
123
  size="sm"
122
124
  className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
123
125
  onClick={onOpen}
124
- title="Atalhos de teclado (Ctrl+/)"
126
+ title={t('triggerTitle')}
125
127
  >
126
128
  <Keyboard className="size-3.5" />
127
- Atalhos
129
+ {t('triggerLabel')}
128
130
  <Badge
129
131
  variant="outline"
130
132
  className="h-4 px-1 text-[0.6rem] font-mono ml-0.5"