@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
@@ -21,6 +21,7 @@ import {
21
21
  ZoomIn,
22
22
  ZoomOut,
23
23
  } from 'lucide-react';
24
+ import { useTranslations } from 'next-intl';
24
25
  import { useCallback, useEffect, useRef } from 'react';
25
26
  import { toast } from 'sonner';
26
27
  import { getCanvasAPI } from '../../_lib/editor/canvasInstance';
@@ -44,6 +45,7 @@ type TopbarProps = {
44
45
  };
45
46
 
46
47
  export default function Topbar({ templateContext }: TopbarProps) {
48
+ const t = useTranslations('lms.CertificateTemplateEditor');
47
49
  const { request } = useApp();
48
50
  const name = useTemplateStore((s) => s.template.name);
49
51
  const setName = useTemplateStore((s) => s.setName);
@@ -96,7 +98,7 @@ export default function Topbar({ templateContext }: TopbarProps) {
96
98
  },
97
99
  })
98
100
  .catch(() => {
99
- toast.error('Falha ao salvar automaticamente');
101
+ toast.error(t('topBar.toasts.autoSaveError'));
100
102
  });
101
103
  }, 1500);
102
104
 
@@ -109,13 +111,13 @@ export default function Topbar({ templateContext }: TopbarProps) {
109
111
  const handleNew = useCallback(() => {
110
112
  resetTemplate();
111
113
  getCanvasAPI()?.loadTemplate(useTemplateStore.getState().template);
112
- toast.success('Novo template criado');
114
+ toast.success(t('topBar.toasts.created'));
113
115
  }, [resetTemplate]);
114
116
 
115
117
  const handleSave = useCallback(() => {
116
118
  const run = async () => {
117
119
  if (!templateContext) {
118
- toast.success('Template salvo no localStorage');
120
+ toast.success(t('topBar.toasts.savedLocalStorage'));
119
121
  return;
120
122
  }
121
123
 
@@ -130,11 +132,11 @@ export default function Topbar({ templateContext }: TopbarProps) {
130
132
  },
131
133
  });
132
134
 
133
- toast.success('Template salvo');
135
+ toast.success(t('topBar.toasts.saved'));
134
136
  };
135
137
 
136
138
  run().catch(() => {
137
- toast.error('Falha ao salvar template');
139
+ toast.error(t('topBar.toasts.saveError'));
138
140
  });
139
141
  }, [request, templateContext, templateState]);
140
142
 
@@ -148,7 +150,7 @@ export default function Topbar({ templateContext }: TopbarProps) {
148
150
  a.download = `${templateState.name.replace(/\s+/g, '_')}.json`;
149
151
  a.click();
150
152
  URL.revokeObjectURL(url);
151
- toast.success('JSON exportado');
153
+ toast.success(t('topBar.toasts.exportedJson'));
152
154
  }, [templateState]);
153
155
 
154
156
  const handleImport = useCallback(
@@ -160,14 +162,14 @@ export default function Topbar({ templateContext }: TopbarProps) {
160
162
  try {
161
163
  const parsed = JSON.parse(ev.target?.result as string);
162
164
  if (!isValidTemplate(parsed)) {
163
- toast.error('JSON invalido: schema ou version incorretos');
165
+ toast.error(t('topBar.toasts.invalidJson'));
164
166
  return;
165
167
  }
166
168
  setTemplate(parsed);
167
169
  getCanvasAPI()?.loadTemplate(parsed);
168
- toast.success('Template importado com sucesso');
170
+ toast.success(t('topBar.toasts.imported'));
169
171
  } catch {
170
- toast.error('Erro ao ler JSON');
172
+ toast.error(t('topBar.toasts.readJsonError'));
171
173
  }
172
174
  };
173
175
  reader.readAsText(file);
@@ -195,7 +197,7 @@ export default function Topbar({ templateContext }: TopbarProps) {
195
197
  value={name}
196
198
  onChange={(e) => setName(e.target.value)}
197
199
  className="h-8 w-52 text-sm font-medium"
198
- aria-label="Nome do template"
200
+ aria-label={t('topBar.templateNameAriaLabel')}
199
201
  />
200
202
 
201
203
  <div className="mx-1 h-5 w-px bg-border" />
@@ -210,10 +212,10 @@ export default function Topbar({ templateContext }: TopbarProps) {
210
212
  onClick={handleNew}
211
213
  >
212
214
  <FilePlus className="size-4" />
213
- <span className="sr-only">Novo</span>
215
+ <span className="sr-only">{t('topBar.actions.new')}</span>
214
216
  </Button>
215
217
  </TooltipTrigger>
216
- <TooltipContent>Novo Template</TooltipContent>
218
+ <TooltipContent>{t('topBar.tooltips.newTemplate')}</TooltipContent>
217
219
  </Tooltip>
218
220
 
219
221
  <Tooltip>
@@ -225,11 +227,13 @@ export default function Topbar({ templateContext }: TopbarProps) {
225
227
  onClick={handleSave}
226
228
  >
227
229
  <Save className="size-4" />
228
- <span className="sr-only">Salvar</span>
230
+ <span className="sr-only">{t('topBar.actions.save')}</span>
229
231
  </Button>
230
232
  </TooltipTrigger>
231
233
  <TooltipContent>
232
- {templateContext ? 'Salvar no banco' : 'Salvar (localStorage)'}
234
+ {templateContext
235
+ ? t('topBar.tooltips.saveDatabase')
236
+ : t('topBar.tooltips.saveLocalStorage')}
233
237
  </TooltipContent>
234
238
  </Tooltip>
235
239
 
@@ -242,10 +246,10 @@ export default function Topbar({ templateContext }: TopbarProps) {
242
246
  onClick={handleExport}
243
247
  >
244
248
  <Download className="size-4" />
245
- <span className="sr-only">Exportar JSON</span>
249
+ <span className="sr-only">{t('topBar.actions.exportJson')}</span>
246
250
  </Button>
247
251
  </TooltipTrigger>
248
- <TooltipContent>Exportar JSON</TooltipContent>
252
+ <TooltipContent>{t('topBar.tooltips.exportJson')}</TooltipContent>
249
253
  </Tooltip>
250
254
 
251
255
  <Tooltip>
@@ -257,10 +261,10 @@ export default function Topbar({ templateContext }: TopbarProps) {
257
261
  onClick={() => importRef.current?.click()}
258
262
  >
259
263
  <Upload className="size-4" />
260
- <span className="sr-only">Importar JSON</span>
264
+ <span className="sr-only">{t('topBar.actions.importJson')}</span>
261
265
  </Button>
262
266
  </TooltipTrigger>
263
- <TooltipContent>Importar JSON</TooltipContent>
267
+ <TooltipContent>{t('topBar.tooltips.importJson')}</TooltipContent>
264
268
  </Tooltip>
265
269
  <input
266
270
  ref={importRef}
@@ -282,11 +286,13 @@ export default function Topbar({ templateContext }: TopbarProps) {
282
286
  onClick={toggleSnap}
283
287
  >
284
288
  <Magnet className="size-4" />
285
- <span className="sr-only">Snap</span>
289
+ <span className="sr-only">{t('topBar.actions.snap')}</span>
286
290
  </Button>
287
291
  </TooltipTrigger>
288
292
  <TooltipContent>
289
- {snapEnabled ? 'Desativar Snap' : 'Ativar Snap'}
293
+ {snapEnabled
294
+ ? t('topBar.tooltips.disableSnap')
295
+ : t('topBar.tooltips.enableSnap')}
290
296
  </TooltipContent>
291
297
  </Tooltip>
292
298
 
@@ -299,11 +305,13 @@ export default function Topbar({ templateContext }: TopbarProps) {
299
305
  onClick={toggleGrid}
300
306
  >
301
307
  <Grid3X3 className="size-4" />
302
- <span className="sr-only">Grid</span>
308
+ <span className="sr-only">{t('topBar.actions.grid')}</span>
303
309
  </Button>
304
310
  </TooltipTrigger>
305
311
  <TooltipContent>
306
- {gridEnabled ? 'Ocultar Grade' : 'Mostrar Grade'}
312
+ {gridEnabled
313
+ ? t('topBar.tooltips.hideGrid')
314
+ : t('topBar.tooltips.showGrid')}
307
315
  </TooltipContent>
308
316
  </Tooltip>
309
317
 
@@ -316,11 +324,13 @@ export default function Topbar({ templateContext }: TopbarProps) {
316
324
  onClick={toggleMargins}
317
325
  >
318
326
  <Square className="size-4" />
319
- <span className="sr-only">Margens</span>
327
+ <span className="sr-only">{t('topBar.actions.margins')}</span>
320
328
  </Button>
321
329
  </TooltipTrigger>
322
330
  <TooltipContent>
323
- {marginsEnabled ? 'Ocultar Margens' : 'Mostrar Margens Seguras'}
331
+ {marginsEnabled
332
+ ? t('topBar.tooltips.hideMargins')
333
+ : t('topBar.tooltips.showMargins')}
324
334
  </TooltipContent>
325
335
  </Tooltip>
326
336
 
@@ -328,13 +338,13 @@ export default function Topbar({ templateContext }: TopbarProps) {
328
338
 
329
339
  {/* ── shortcuts hint ── */}
330
340
  <div className="hidden items-center gap-2 text-[10px] text-muted-foreground lg:flex">
331
- <span>Ctrl+Scroll: zoom</span>
341
+ <span>{t('topBar.shortcuts.ctrlScrollZoom')}</span>
332
342
  <span className="text-border">|</span>
333
- <span>Space+Drag: pan</span>
343
+ <span>{t('topBar.shortcuts.spaceDragPan')}</span>
334
344
  <span className="text-border">|</span>
335
- <span>Del: excluir</span>
345
+ <span>{t('topBar.shortcuts.delDelete')}</span>
336
346
  <span className="text-border">|</span>
337
- <span>Ctrl+D: duplicar</span>
347
+ <span>{t('topBar.shortcuts.ctrlDDuplicate')}</span>
338
348
  </div>
339
349
 
340
350
  <div className="mx-1 h-5 w-px bg-border" />
@@ -350,10 +360,10 @@ export default function Topbar({ templateContext }: TopbarProps) {
350
360
  onClick={zoomOut}
351
361
  >
352
362
  <ZoomOut className="size-4" />
353
- <span className="sr-only">Zoom Out</span>
363
+ <span className="sr-only">{t('topBar.actions.zoomOut')}</span>
354
364
  </Button>
355
365
  </TooltipTrigger>
356
- <TooltipContent>Zoom -</TooltipContent>
366
+ <TooltipContent>{t('topBar.tooltips.zoomOut')}</TooltipContent>
357
367
  </Tooltip>
358
368
 
359
369
  <span className="w-14 text-center text-xs font-medium tabular-nums text-muted-foreground">
@@ -369,10 +379,10 @@ export default function Topbar({ templateContext }: TopbarProps) {
369
379
  onClick={zoomIn}
370
380
  >
371
381
  <ZoomIn className="size-4" />
372
- <span className="sr-only">Zoom In</span>
382
+ <span className="sr-only">{t('topBar.actions.zoomIn')}</span>
373
383
  </Button>
374
384
  </TooltipTrigger>
375
- <TooltipContent>Zoom +</TooltipContent>
385
+ <TooltipContent>{t('topBar.tooltips.zoomIn')}</TooltipContent>
376
386
  </Tooltip>
377
387
 
378
388
  <Tooltip>
@@ -384,10 +394,10 @@ export default function Topbar({ templateContext }: TopbarProps) {
384
394
  onClick={zoomReset}
385
395
  >
386
396
  <RotateCcw className="size-4" />
387
- <span className="sr-only">Reset Zoom</span>
397
+ <span className="sr-only">{t('topBar.actions.resetZoom')}</span>
388
398
  </Button>
389
399
  </TooltipTrigger>
390
- <TooltipContent>Reset Zoom + Pan (50%)</TooltipContent>
400
+ <TooltipContent>{t('topBar.tooltips.resetZoom')}</TooltipContent>
391
401
  </Tooltip>
392
402
  </div>
393
403
  </header>
@@ -1190,7 +1190,7 @@ export default function TurmaDetalhePage() {
1190
1190
  setGlobalMaterials((prev) => [res.data, ...prev]);
1191
1191
  resetGlobalLinkForm();
1192
1192
  } catch {
1193
- toast.error('Não foi possível adicionar o link.');
1193
+ toast.error(t('messages.linkAddError'));
1194
1194
  } finally {
1195
1195
  setGlobalSavingLink(false);
1196
1196
  }
@@ -1265,7 +1265,7 @@ export default function TurmaDetalhePage() {
1265
1265
  });
1266
1266
  setGlobalMaterials((prev) => prev.filter((m) => m.id !== materialId));
1267
1267
  } catch {
1268
- toast.error('Não foi possível remover o material.');
1268
+ toast.error(t('messages.materialRemoveError'));
1269
1269
  }
1270
1270
  };
1271
1271
 
@@ -1653,9 +1653,9 @@ export default function TurmaDetalhePage() {
1653
1653
  });
1654
1654
  await refetchCourseDetail();
1655
1655
  setCourseSheetOpen(false);
1656
- toast.success('Curso atualizado com sucesso.');
1656
+ toast.success(t('messages.courseUpdateSuccess'));
1657
1657
  } catch {
1658
- toast.error('Não foi possível salvar o curso.');
1658
+ toast.error(t('messages.courseSaveError'));
1659
1659
  } finally {
1660
1660
  setSavingCourse(false);
1661
1661
  }
@@ -1690,7 +1690,7 @@ export default function TurmaDetalhePage() {
1690
1690
  );
1691
1691
 
1692
1692
  if (selectedEligiblePeople.length === 0) {
1693
- toast.error('Nenhuma pessoa elegivel foi encontrada para matricula.');
1693
+ toast.error(t('messages.noEligiblePersonFound'));
1694
1694
  return;
1695
1695
  }
1696
1696
 
@@ -1718,7 +1718,7 @@ export default function TurmaDetalhePage() {
1718
1718
  const message = getErrorMessage(error);
1719
1719
 
1720
1720
  if (message?.toLowerCase().includes('already enrolled')) {
1721
- toast.error('Esta pessoa ja esta matriculada na turma.');
1721
+ toast.error(t('messages.personAlreadyEnrolled'));
1722
1722
  } else {
1723
1723
  toast.error(message || t('toasts.error'));
1724
1724
  }
@@ -2208,7 +2208,7 @@ export default function TurmaDetalhePage() {
2208
2208
  notifyLmsDataUpdated();
2209
2209
  setDeleteAulaDialogOpen(false);
2210
2210
  setAulaToDelete(null);
2211
- toast.success('Aula removida com sucesso.');
2211
+ toast.success(t('messages.lessonRemovedSuccess'));
2212
2212
  } catch {
2213
2213
  toast.error(t('toasts.error'));
2214
2214
  } finally {
@@ -2359,7 +2359,7 @@ export default function TurmaDetalhePage() {
2359
2359
  }));
2360
2360
  resetLinkForm();
2361
2361
  } catch {
2362
- toast.error('Não foi possível adicionar o link.');
2362
+ toast.error(t('messages.linkAddError'));
2363
2363
  } finally {
2364
2364
  setSavingLink(false);
2365
2365
  }
@@ -2379,7 +2379,7 @@ export default function TurmaDetalhePage() {
2379
2379
  }));
2380
2380
  }
2381
2381
  } catch {
2382
- toast.error('Não foi possível remover o material.');
2382
+ toast.error(t('messages.materialRemoveError'));
2383
2383
  }
2384
2384
  };
2385
2385
 
@@ -2522,7 +2522,7 @@ export default function TurmaDetalhePage() {
2522
2522
 
2523
2523
  const handleViewCourse = (): void => {
2524
2524
  if (!courseId) {
2525
- toast.error('Nao foi possivel localizar o curso desta turma.');
2525
+ toast.error(t('messages.classCourseNotFound'));
2526
2526
  return;
2527
2527
  }
2528
2528
 
@@ -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
  >