@hed-hog/lms 0.0.329 → 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 (37) hide show
  1. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
  2. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +7 -5
  3. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
  4. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
  5. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
  6. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
  7. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
  8. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
  9. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +10 -10
  10. package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
  11. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +5 -3
  12. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
  13. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +9 -7
  14. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +3 -1
  15. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +4 -2
  16. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +24 -23
  17. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
  18. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +7 -5
  19. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
  20. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
  21. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
  22. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
  23. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
  24. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
  25. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
  26. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
  27. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
  28. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
  29. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
  30. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
  31. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
  32. package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
  33. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
  34. package/hedhog/frontend/app/instructors/page.tsx.ejs +4 -2
  35. package/hedhog/frontend/messages/en.json +619 -13
  36. package/hedhog/frontend/messages/pt.json +619 -13
  37. package/package.json +7 -7
@@ -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"
@@ -10,6 +10,7 @@ import {
10
10
  import { Separator } from '@/components/ui/separator';
11
11
  import { Switch } from '@/components/ui/switch';
12
12
  import { SlidersHorizontal } from 'lucide-react';
13
+ import { useTranslations } from 'next-intl';
13
14
 
14
15
  import { useTreeDisplaySettings } from './use-tree-display-settings';
15
16
 
@@ -18,6 +19,7 @@ import { useTreeDisplaySettings } from './use-tree-display-settings';
18
19
  // ─────────────────────────────────────────────────────────────────────────────
19
20
 
20
21
  export function TreeDisplaySettingsPopover() {
22
+ const t = useTranslations('lms.CoursesPage.StructurePage.displaySettings');
21
23
  const {
22
24
  showStatusDot,
23
25
  showVisibility,
@@ -35,26 +37,26 @@ export function TreeDisplaySettingsPopover() {
35
37
  variant="ghost"
36
38
  size="sm"
37
39
  className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
38
- title="Configurações de exibição da lista"
40
+ title={t('title')}
39
41
  >
40
42
  <SlidersHorizontal className="size-3.5" />
41
- <span className="sr-only sm:not-sr-only">Exibição</span>
43
+ <span className="sr-only sm:not-sr-only">{t('label')}</span>
42
44
  </Button>
43
45
  </PopoverTrigger>
44
46
 
45
47
  <PopoverContent align="end" className="w-56 p-0">
46
48
  <div className="px-3 py-2.5 border-b">
47
- <p className="text-xs font-semibold">Informações nas aulas</p>
49
+ <p className="text-xs font-semibold">{t('sectionTitle')}</p>
48
50
  <p className="text-[0.65rem] text-muted-foreground mt-0.5">
49
- Escolha o que exibir em cada linha
51
+ {t('sectionDescription')}
50
52
  </p>
51
53
  </div>
52
54
 
53
55
  <div className="flex flex-col gap-0">
54
56
  <SettingRow
55
57
  id="show-status-dot"
56
- label="Dot de status"
57
- description="Cor indicando etapa de produção"
58
+ label={t('showStatusDot.label')}
59
+ description={t('showStatusDot.description')}
58
60
  checked={showStatusDot}
59
61
  onCheckedChange={(v) => update({ showStatusDot: v })}
60
62
  />
@@ -63,8 +65,8 @@ export function TreeDisplaySettingsPopover() {
63
65
 
64
66
  <SettingRow
65
67
  id="show-visibility"
66
- label="Visibilidade"
67
- description="Ícone de público / privado / restrito"
68
+ label={t('showVisibility.label')}
69
+ description={t('showVisibility.description')}
68
70
  checked={showVisibility}
69
71
  onCheckedChange={(v) => update({ showVisibility: v })}
70
72
  />
@@ -73,8 +75,8 @@ export function TreeDisplaySettingsPopover() {
73
75
 
74
76
  <SettingRow
75
77
  id="show-code"
76
- label="Código"
77
- description="Exibe o código identificador"
78
+ label={t('showCode.label')}
79
+ description={t('showCode.description')}
78
80
  checked={showCode}
79
81
  onCheckedChange={(v) => update({ showCode: v })}
80
82
  />
@@ -83,8 +85,8 @@ export function TreeDisplaySettingsPopover() {
83
85
 
84
86
  <SettingRow
85
87
  id="show-video-indicator"
86
- label="Vídeo vinculado"
87
- description="Ícone quando aula de vídeo tem URL definida"
88
+ label={t('showVideoIndicator.label')}
89
+ description={t('showVideoIndicator.description')}
88
90
  checked={showVideoIndicator}
89
91
  onCheckedChange={(v) => update({ showVideoIndicator: v })}
90
92
  />
@@ -93,8 +95,8 @@ export function TreeDisplaySettingsPopover() {
93
95
 
94
96
  <SettingRow
95
97
  id="show-resources-indicator"
96
- label="Recursos para download"
97
- description="Ícone quando há arquivos anexados"
98
+ label={t('showResourcesIndicator.label')}
99
+ description={t('showResourcesIndicator.description')}
98
100
  checked={showResourcesIndicator}
99
101
  onCheckedChange={(v) => update({ showResourcesIndicator: v })}
100
102
  />
@@ -103,8 +105,8 @@ export function TreeDisplaySettingsPopover() {
103
105
 
104
106
  <SettingRow
105
107
  id="show-transcription-indicator"
106
- label="Transcrição"
107
- description="Ícone quando vídeo tem transcrição"
108
+ label={t('showTranscriptionIndicator.label')}
109
+ description={t('showTranscriptionIndicator.description')}
108
110
  checked={showTranscriptionIndicator}
109
111
  onCheckedChange={(v) => update({ showTranscriptionIndicator: v })}
110
112
  />
@@ -15,6 +15,7 @@ import {
15
15
  Video,
16
16
  type LucideIcon,
17
17
  } from 'lucide-react';
18
+ import { useTranslations } from 'next-intl';
18
19
  import { useEffect, useRef } from 'react';
19
20
  import { useUpdateLessonMutation } from '../_data/use-course-structure-mutations';
20
21
  import { HighlightedText } from './highlighted-text';
@@ -75,6 +76,7 @@ export function TreeRowLesson({
75
76
  query,
76
77
  onClick,
77
78
  }: TreeRowLessonProps) {
79
+ const t = useTranslations('lms.CoursesPage.StructurePage.lessonRow');
78
80
  const cfg = LESSON_TYPE_CONFIG[data.type];
79
81
  const Icon = cfg.icon;
80
82
 
@@ -181,7 +183,7 @@ export function TreeRowLesson({
181
183
  onClick={(e) => e.stopPropagation()}
182
184
  onBlur={(e) => commitAndPersist(e.target.value)}
183
185
  onKeyDown={handleKeyDown}
184
- aria-label="Renomear aula"
186
+ aria-label={t('rename')}
185
187
  />
186
188
  ) : (
187
189
  <span className="truncate flex-1 text-xs leading-tight">
@@ -209,8 +211,8 @@ export function TreeRowLesson({
209
211
  const VisIcon = visibilityCfg.icon;
210
212
  return (
211
213
  <span
212
- title={`Visibilidade: ${visibilityCfg.label}`}
213
- aria-label={`Visibilidade: ${visibilityCfg.label}`}
214
+ title={t('visibility', { value: visibilityCfg.label })}
215
+ aria-label={t('visibility', { value: visibilityCfg.label })}
214
216
  className="inline-flex items-center"
215
217
  >
216
218
  <VisIcon
@@ -223,15 +225,15 @@ export function TreeRowLesson({
223
225
  {showStatusDot && statusCfg && (
224
226
  <span
225
227
  className={cn('size-1.5 rounded-full shrink-0', statusCfg.dot)}
226
- title={`Status: ${statusCfg.label}`}
227
- aria-label={`Status: ${statusCfg.label}`}
228
+ title={t('status', { value: statusCfg.label })}
229
+ aria-label={t('status', { value: statusCfg.label })}
228
230
  />
229
231
  )}
230
232
  {/* Video linked indicator */}
231
233
  {showVideoIndicator && data.type === 'video' && data.videoUrl && (
232
234
  <span
233
- title="Vídeo vinculado"
234
- aria-label="Vídeo vinculado"
235
+ title={t('videoLinked')}
236
+ aria-label={t('videoLinked')}
235
237
  className="inline-flex items-center"
236
238
  >
237
239
  <Film className="size-3 shrink-0 text-violet-500" />
@@ -240,8 +242,8 @@ export function TreeRowLesson({
240
242
  {/* Resources indicator */}
241
243
  {showResourcesIndicator && data.resources.length > 0 && (
242
244
  <span
243
- title={`${data.resources.length} recurso${data.resources.length > 1 ? 's' : ''} para download`}
244
- aria-label={`${data.resources.length} recursos`}
245
+ title={t('resources', { count: data.resources.length })}
246
+ aria-label={t('resourcesAria', { count: data.resources.length })}
245
247
  className="inline-flex items-center gap-0.5"
246
248
  >
247
249
  <Paperclip className="size-3 shrink-0 text-sky-500" />
@@ -257,8 +259,8 @@ export function TreeRowLesson({
257
259
  data.type === 'video' &&
258
260
  data.transcription && (
259
261
  <span
260
- title="Possui transcrição"
261
- aria-label="Possui transcrição"
262
+ title={t('hasTranscription')}
263
+ aria-label={t('hasTranscription')}
262
264
  className="inline-flex items-center"
263
265
  >
264
266
  <ScrollText className="size-3 shrink-0 text-emerald-500" />
@@ -3,6 +3,7 @@
3
3
  import { Input } from '@/components/ui/input';
4
4
  import { cn } from '@/lib/utils';
5
5
  import { ChevronDown, ChevronRight, Layers } from 'lucide-react';
6
+ import { useTranslations } from 'next-intl';
6
7
  import { useEffect, useRef } from 'react';
7
8
  import { useUpdateSessionMutation } from '../_data/use-course-structure-mutations';
8
9
  import { HighlightedText } from './highlighted-text';
@@ -31,6 +32,7 @@ export function TreeRowSession({
31
32
  onClick,
32
33
  onToggleExpand,
33
34
  }: TreeRowSessionProps) {
35
+ const t = useTranslations('lms.CoursesPage.StructurePage.sessionRow');
34
36
  const inlineRenamingId = useStructureStore((s) => s.inlineRenamingId);
35
37
  const cancelRename = useStructureStore((s) => s.cancelRename);
36
38
  const startRename = useStructureStore((s) => s.startRename);
@@ -104,7 +106,7 @@ export function TreeRowSession({
104
106
  <button
105
107
  className="shrink-0 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded focus-visible:ring-1 focus-visible:ring-ring"
106
108
  onClick={onToggleExpand}
107
- aria-label={isExpanded ? 'Recolher sessão' : 'Expandir sessão'}
109
+ aria-label={isExpanded ? t('collapse') : t('expand')}
108
110
  tabIndex={-1}
109
111
  >
110
112
  {isExpanded ? (
@@ -129,7 +131,7 @@ export function TreeRowSession({
129
131
  onClick={(e) => e.stopPropagation()}
130
132
  onBlur={(e) => commitAndPersist(e.target.value)}
131
133
  onKeyDown={handleKeyDown}
132
- aria-label="Renomear sessão"
134
+ aria-label={t('rename')}
133
135
  />
134
136
  ) : (
135
137
  <span className="truncate flex-1 font-medium leading-tight">
@@ -158,7 +160,7 @@ export function TreeRowSession({
158
160
  ? 'bg-muted-foreground/30'
159
161
  : 'bg-emerald-500'
160
162
  )}
161
- title={data.published === false ? 'Rascunho' : 'Publicada'}
163
+ title={data.published === false ? t('draft') : t('published')}
162
164
  />
163
165
  </div>
164
166
  )}
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useRef } from 'react';
4
+ import { useTranslations } from 'next-intl';
4
5
  import { toast } from 'sonner';
5
6
 
6
7
  import type { SearchFilterHandle } from './search-filter';
@@ -44,6 +45,7 @@ export function useCourseStructureShortcuts({
44
45
  onDuplicate,
45
46
  onPaste,
46
47
  }: UseCourseStructureShortcutsOptions) {
48
+ const t = useTranslations('lms.CoursesPage.StructurePage.shortcuts');
47
49
  const course = useStructureStore((s) => s.course);
48
50
  const sessions = useStructureStore((s) => s.sessions);
49
51
  const lessons = useStructureStore((s) => s.lessons);
@@ -236,12 +238,15 @@ export function useCourseStructureShortcuts({
236
238
  if (!ids.length) return;
237
239
  const count = ids.length;
238
240
  s.showConfirm({
239
- title: `Excluir ${count > 1 ? count + ' itens' : 'item selecionado'}?`,
240
- description: 'Esta ação não pode ser desfeita.',
241
+ title: t('deleteTitle', {
242
+ label:
243
+ count > 1 ? `${count} ${t('items')}` : t('selectedItem'),
244
+ }),
245
+ description: t('deleteDescription'),
241
246
  onConfirm: () => {
242
247
  s.deleteSelected();
243
248
  toast.success(
244
- count > 1 ? `${count} itens excluídos` : 'Item excluído'
249
+ count > 1 ? t('itemsDeleted', { count }) : t('itemDeleted')
245
250
  );
246
251
  },
247
252
  });
@@ -260,7 +265,7 @@ export function useCourseStructureShortcuts({
260
265
  const ids = selectedKeys.map((key) => key.slice(key.indexOf(':') + 1));
261
266
  s.copyItems(ids, type);
262
267
  toast.success(
263
- `${ids.length > 1 ? ids.length + ' itens copiados' : 'Item copiado'}`
268
+ ids.length > 1 ? t('itemsCopied', { count: ids.length }) : t('itemCopied')
264
269
  );
265
270
  return;
266
271
  }
@@ -272,7 +277,7 @@ export function useCourseStructureShortcuts({
272
277
  onDuplicate();
273
278
  } else {
274
279
  s.duplicateSelected();
275
- toast.success('Item duplicado');
280
+ toast.success(t('itemDuplicated'));
276
281
  }
277
282
  return;
278
283
  }
@@ -283,7 +288,7 @@ export function useCourseStructureShortcuts({
283
288
  if (onPaste) {
284
289
  onPaste();
285
290
  } else {
286
- toast('Nada para colar');
291
+ toast(t('nothingToPaste'));
287
292
  }
288
293
  return;
289
294
  }
@@ -293,16 +298,16 @@ export function useCourseStructureShortcuts({
293
298
  e.preventDefault();
294
299
  if (s.activeItemType === 'course') {
295
300
  s.addSession();
296
- toast.success('Nova sessão criada');
301
+ toast.success(t('newSessionCreated'));
297
302
  } else if (s.activeItemType === 'session' && s.activeItemId) {
298
303
  s.addLesson(s.activeItemId);
299
- toast.success('Nova aula criada');
304
+ toast.success(t('newLessonCreated'));
300
305
  } else if (s.activeItemType === 'lesson') {
301
306
  // Find parent session and add there
302
307
  const lesson = s.lessons.find((l) => l.id === s.activeItemId);
303
308
  if (lesson) {
304
309
  s.addLesson(lesson.sessionId);
305
- toast.success('Nova aula criada na mesma sessão');
310
+ toast.success(t('newLessonSameSession'));
306
311
  }
307
312
  }
308
313
  return;