@hed-hog/lms 0.0.330 → 0.0.331

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 (31) hide show
  1. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
  2. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +1 -1
  3. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +17 -17
  4. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +2 -2
  5. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +3 -3
  6. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +1 -1
  7. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +228 -152
  8. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +71 -31
  9. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +37 -41
  10. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
  11. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -2
  12. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +145 -119
  13. package/hedhog/frontend/app/instructors/page.tsx.ejs +71 -52
  14. package/hedhog/frontend/app/paths/page.tsx.ejs +11 -7
  15. package/hedhog/frontend/app/reports/courses/page.tsx.ejs +5 -5
  16. package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +8 -8
  17. package/hedhog/frontend/app/reports/page.tsx.ejs +7 -7
  18. package/hedhog/frontend/app/reports/students/page.tsx.ejs +6 -6
  19. package/hedhog/frontend/app/training/page.tsx.ejs +5 -5
  20. package/hedhog/frontend/messages/en.json +294 -46
  21. package/hedhog/frontend/messages/pt.json +289 -39
  22. package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +1 -1
  23. package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +1 -1
  24. package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +1 -1
  25. package/hedhog/frontend/widgets/class-calendar.tsx.ejs +2 -2
  26. package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +1 -1
  27. package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +1 -1
  28. package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +1 -1
  29. package/hedhog/table/instructor_qualification.yaml +1 -1
  30. package/hedhog/table/instructor_skill.yaml +1 -1
  31. package/package.json +7 -7
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { zodResolver } from '@hookform/resolvers/zod';
4
+ import { useTranslations } from 'next-intl';
4
5
  import {
5
6
  CircleDot,
6
7
  CircleOff,
@@ -131,42 +132,34 @@ function formatFileSize(bytes: number): string {
131
132
 
132
133
  const TYPE_CONFIG: Record<
133
134
  LessonType,
134
- { icon: LucideIcon; color: string; bg: string; label: string }
135
+ { icon: LucideIcon; color: string; bg: string; labelKey: string }
135
136
  > = {
136
137
  video: {
137
138
  icon: Video,
138
139
  color: 'text-blue-500',
139
140
  bg: 'bg-blue-500/10',
140
- label: 'Vídeo',
141
+ labelKey: 'types.video',
141
142
  },
142
143
  post: {
143
144
  icon: FileText,
144
145
  color: 'text-emerald-500',
145
146
  bg: 'bg-emerald-500/10',
146
- label: 'Texto',
147
+ labelKey: 'types.post',
147
148
  },
148
149
  questao: {
149
150
  icon: HelpCircle,
150
151
  color: 'text-amber-500',
151
152
  bg: 'bg-amber-500/10',
152
- label: 'Quiz',
153
+ labelKey: 'types.questao',
153
154
  },
154
155
  exercicio: {
155
156
  icon: ClipboardList,
156
157
  color: 'text-purple-500',
157
158
  bg: 'bg-purple-500/10',
158
- label: 'Exercício',
159
+ labelKey: 'types.exercicio',
159
160
  },
160
161
  };
161
162
 
162
- const STATUS_LABELS: Record<LessonStatus, string> = {
163
- preparada: 'Preparada',
164
- gravada: 'Gravada',
165
- editada: 'Editada',
166
- finalizada: 'Finalizada',
167
- publicada: 'Publicada',
168
- };
169
-
170
163
  const STATUS_COLORS: Record<LessonStatus, string> = {
171
164
  preparada:
172
165
  'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300',
@@ -180,13 +173,6 @@ const STATUS_COLORS: Record<LessonStatus, string> = {
180
173
  'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
181
174
  };
182
175
 
183
- const VIDEO_PROVIDERS: { value: VideoProvider; label: string }[] = [
184
- { value: 'youtube', label: 'YouTube' },
185
- { value: 'vimeo', label: 'Vimeo' },
186
- { value: 'bunny', label: 'Bunny.net' },
187
- { value: 'custom', label: 'URL Direta' },
188
- ];
189
-
190
176
  // ── Question sheet types ──────────────────────────────────────────────────────
191
177
 
192
178
  type QuestionType =
@@ -196,14 +182,6 @@ type QuestionType =
196
182
  | 'fill_blank'
197
183
  | 'matching';
198
184
 
199
- const QUESTION_TYPE_LABELS: Record<QuestionType, string> = {
200
- multiple_choice: 'Múltipla escolha',
201
- true_false: 'Verdadeiro / Falso',
202
- essay: 'Dissertativa',
203
- fill_blank: 'Lacuna',
204
- matching: 'Associação',
205
- };
206
-
207
185
  type QAlt = { id: string; texto: string; correta: boolean };
208
186
  type QFillBlank = { id: string; answer: string; alternativesText: string };
209
187
  type QMatchingPair = { id: string; leftText: string; rightText: string };
@@ -257,33 +235,21 @@ const MOCK_QUESTIONS: MockQuestion[] = [
257
235
  },
258
236
  ];
259
237
 
260
- // ── Schema ────────────────────────────────────────────────────────────────────
261
-
262
- const schema = z.object({
263
- code: z.string().min(1, 'Código obrigatório'),
264
- title: z.string().min(1, 'Título obrigatório'),
265
- type: z.enum(['video', 'post', 'questao', 'exercicio'] as const),
266
- duration: z.coerce.number().min(0),
267
- status: z.enum([
268
- 'preparada',
269
- 'gravada',
270
- 'editada',
271
- 'finalizada',
272
- 'publicada',
273
- ] as const),
274
- visibility: z.enum(['publico', 'privado', 'restrito'] as const),
275
- publicDescription: z.string(),
276
- privateDescription: z.string(),
277
- videoProvider: z
278
- .enum(['youtube', 'vimeo', 'bunny', 'custom'] as const)
279
- .optional(),
280
- videoUrl: z.string().optional(),
281
- transcription: z.string().optional(),
282
- postContent: z.string().optional(),
283
- questionId: z.string().nullable().optional(),
284
- });
285
-
286
- type FormValues = z.infer<typeof schema>;
238
+ type FormValues = {
239
+ code: string;
240
+ title: string;
241
+ type: LessonType;
242
+ duration: number;
243
+ status: LessonStatus;
244
+ visibility: 'publico' | 'privado' | 'restrito';
245
+ publicDescription: string;
246
+ privateDescription: string;
247
+ videoProvider?: VideoProvider;
248
+ videoUrl?: string;
249
+ transcription?: string;
250
+ postContent?: string;
251
+ questionId?: string | null;
252
+ };
287
253
 
288
254
  // ── SortableAlternativa ───────────────────────────────────────────────────────
289
255
 
@@ -295,6 +261,11 @@ function SortableAlternativa({
295
261
  onRemove,
296
262
  canRemove,
297
263
  disableText,
264
+ dragLabel,
265
+ textPlaceholder,
266
+ markIncorrectLabel,
267
+ markCorrectLabel,
268
+ removeLabel,
298
269
  }: {
299
270
  alt: QAlt;
300
271
  index: number;
@@ -303,6 +274,11 @@ function SortableAlternativa({
303
274
  onRemove: () => void;
304
275
  canRemove: boolean;
305
276
  disableText?: boolean;
277
+ dragLabel: string;
278
+ textPlaceholder: string;
279
+ markIncorrectLabel: string;
280
+ markCorrectLabel: string;
281
+ removeLabel: string;
306
282
  }) {
307
283
  const {
308
284
  attributes,
@@ -328,7 +304,7 @@ function SortableAlternativa({
328
304
  className="cursor-grab touch-none text-muted-foreground hover:text-foreground"
329
305
  {...attributes}
330
306
  {...listeners}
331
- aria-label="Arrastar alternativa"
307
+ aria-label={dragLabel}
332
308
  >
333
309
  <GripVertical className="size-4" />
334
310
  </button>
@@ -339,7 +315,7 @@ function SortableAlternativa({
339
315
  value={alt.texto}
340
316
  onChange={(e) => onChangeTexto(e.target.value)}
341
317
  className="flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0"
342
- placeholder="Texto da alternativa…"
318
+ placeholder={textPlaceholder}
343
319
  disabled={disableText}
344
320
  />
345
321
  <button
@@ -352,7 +328,7 @@ function SortableAlternativa({
352
328
  : 'text-muted-foreground hover:text-foreground'
353
329
  )}
354
330
  aria-label={
355
- alt.correta ? 'Marcar como incorreta' : 'Marcar como correta'
331
+ alt.correta ? markIncorrectLabel : markCorrectLabel
356
332
  }
357
333
  >
358
334
  {alt.correta ? (
@@ -366,7 +342,7 @@ function SortableAlternativa({
366
342
  type="button"
367
343
  onClick={onRemove}
368
344
  className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-destructive"
369
- aria-label="Remover alternativa"
345
+ aria-label={removeLabel}
370
346
  >
371
347
  <X className="size-4" />
372
348
  </button>
@@ -382,6 +358,7 @@ interface EditorLessonProps {
382
358
  }
383
359
 
384
360
  export function EditorLesson({ lessonId }: EditorLessonProps) {
361
+ const t = useTranslations('lms.CoursesPage.StructurePage');
385
362
  const lesson = useStructureStore((s) =>
386
363
  s.lessons.find((l) => l.id === lessonId)
387
364
  );
@@ -391,6 +368,49 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
391
368
  const courseId = useStructureStore((s) => s.courseId);
392
369
  const { request } = useApp();
393
370
  const queryClient = useQueryClient();
371
+ const statusLabels: Record<LessonStatus, string> = {
372
+ preparada: t('statuses.preparada'),
373
+ gravada: t('statuses.gravada'),
374
+ editada: t('statuses.editada'),
375
+ finalizada: t('statuses.finalizada'),
376
+ publicada: t('statuses.publicada'),
377
+ };
378
+ const videoProviders: { value: VideoProvider; label: string }[] = [
379
+ { value: 'youtube', label: 'YouTube' },
380
+ { value: 'vimeo', label: 'Vimeo' },
381
+ { value: 'bunny', label: 'Bunny.net' },
382
+ { value: 'custom', label: t('providers.custom') },
383
+ ];
384
+ const questionTypeLabels: Record<QuestionType, string> = {
385
+ multiple_choice: t('questionEditor.types.multipleChoice'),
386
+ true_false: t('questionEditor.types.trueFalse'),
387
+ essay: t('questionEditor.types.essay'),
388
+ fill_blank: t('questionEditor.types.fillBlank'),
389
+ matching: t('questionEditor.types.matching'),
390
+ };
391
+ const schema = z.object({
392
+ code: z.string().min(1, t('questionEditor.validation.codeRequired')),
393
+ title: z.string().min(1, t('questionEditor.validation.titleRequired')),
394
+ type: z.enum(['video', 'post', 'questao', 'exercicio'] as const),
395
+ duration: z.coerce.number().min(0),
396
+ status: z.enum([
397
+ 'preparada',
398
+ 'gravada',
399
+ 'editada',
400
+ 'finalizada',
401
+ 'publicada',
402
+ ] as const),
403
+ visibility: z.enum(['publico', 'privado', 'restrito'] as const),
404
+ publicDescription: z.string(),
405
+ privateDescription: z.string(),
406
+ videoProvider: z
407
+ .enum(['youtube', 'vimeo', 'bunny', 'custom'] as const)
408
+ .optional(),
409
+ videoUrl: z.string().optional(),
410
+ transcription: z.string().optional(),
411
+ postContent: z.string().optional(),
412
+ questionId: z.string().nullable().optional(),
413
+ });
394
414
 
395
415
  const instructorPool =
396
416
  queryClient.getQueryData<CourseStructureCacheData>(
@@ -469,6 +489,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
469
489
 
470
490
  const cfg = TYPE_CONFIG[lesson.type];
471
491
  const Icon = cfg.icon;
492
+ const lessonTypeLabel = t(cfg.labelKey as any);
472
493
 
473
494
  async function handleResourceFiles(files: File[]) {
474
495
  setIsUploading(true);
@@ -493,7 +514,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
493
514
  const failedCount = results.filter((r) => r.status === 'rejected').length;
494
515
  if (failedCount > 0)
495
516
  toast.error(
496
- `${failedCount} arquivo${failedCount > 1 ? 's' : ''} não ${failedCount > 1 ? 'puderam' : 'pôde'} ser enviado${failedCount > 1 ? 's' : ''}`
517
+ t('questionEditor.resourceUploadFailed', { count: failedCount })
497
518
  );
498
519
  if (succeeded.length > 0)
499
520
  setLocalResources((prev) => [...prev, ...succeeded]);
@@ -508,7 +529,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
508
529
  try {
509
530
  await deleteFile(request, numId);
510
531
  } catch {
511
- toast.error('Erro ao remover arquivo');
532
+ toast.error(t('questionEditor.resourceRemoveError'));
512
533
  return;
513
534
  }
514
535
  } else {
@@ -520,7 +541,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
520
541
 
521
542
  function handleResourceDownload(res: Resource) {
522
543
  if (!res.url) {
523
- toast(`Download de ${res.name} — em breve`);
544
+ toast(t('questionEditor.resourceDownloadSoon', { name: res.name }));
524
545
  return;
525
546
  }
526
547
  const a = document.createElement('a');
@@ -612,12 +633,13 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
612
633
 
613
634
  function saveQuestion() {
614
635
  const errors: Record<string, string> = {};
615
- if (!qSheetStatement.trim()) errors.statement = 'Enunciado obrigatório';
636
+ if (!qSheetStatement.trim())
637
+ errors.statement = t('questionEditor.validation.statementRequired');
616
638
  if (
617
639
  (qSheetType === 'multiple_choice' || qSheetType === 'true_false') &&
618
640
  !qSheetAlts.some((a) => a.correta)
619
641
  )
620
- errors.alts = 'Selecione pelo menos uma resposta correta';
642
+ errors.alts = t('questionEditor.validation.correctAnswerRequired');
621
643
  if (Object.keys(errors).length) {
622
644
  setQSheetErrors(errors);
623
645
  return;
@@ -639,13 +661,17 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
639
661
  setSelectedQuestion(saved);
640
662
  form.setValue('questionId', saved.id, { shouldDirty: true });
641
663
  setQuestionSheetOpen(false);
642
- toast.success(editingQuestion ? 'Questão atualizada' : 'Questão criada');
664
+ toast.success(
665
+ editingQuestion
666
+ ? t('questionEditor.updated')
667
+ : t('questionEditor.created')
668
+ );
643
669
  }
644
670
 
645
671
  function handleDelete() {
646
672
  showConfirm({
647
- title: `Excluir aula "${lesson!.title}"?`,
648
- description: 'Esta ação não pode ser desfeita.',
673
+ title: `${t('deleteLesson.title')} "${lesson!.title}"?`,
674
+ description: t('deleteLesson.warning'),
649
675
  onConfirm: () =>
650
676
  deleteLesson.mutate({ lessonId, sessionId: lesson!.sessionId }),
651
677
  });
@@ -669,13 +695,15 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
669
695
  </div>
670
696
  <div className="flex-1 min-w-0">
671
697
  <div className="flex items-center gap-1.5">
672
- <span className="text-sm font-semibold truncate">Aula</span>
698
+ <span className="text-sm font-semibold truncate">
699
+ {t('lessonForm.titleCreate')}
700
+ </span>
673
701
  {isDirty && (
674
702
  <CircleDot className="size-3 text-amber-500 shrink-0" />
675
703
  )}
676
704
  </div>
677
705
  <p className="text-[0.65rem] text-muted-foreground truncate">
678
- {lesson.code} · {cfg.label}
706
+ {lesson.code} · {lessonTypeLabel}
679
707
  </p>
680
708
  </div>
681
709
  {watchedStatus && (
@@ -685,7 +713,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
685
713
  STATUS_COLORS[watchedStatus]
686
714
  )}
687
715
  >
688
- {STATUS_LABELS[watchedStatus]}
716
+ {statusLabels[watchedStatus]}
689
717
  </span>
690
718
  )}
691
719
  <Button
@@ -693,8 +721,8 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
693
721
  variant="ghost"
694
722
  size="icon"
695
723
  className="size-7 text-destructive/60 hover:text-destructive shrink-0"
696
- title="Excluir aula"
697
- aria-label="Excluir aula"
724
+ title={t('lesson.delete')}
725
+ aria-label={t('lesson.delete')}
698
726
  disabled={deleteLesson.isPending}
699
727
  onClick={handleDelete}
700
728
  >
@@ -710,13 +738,13 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
710
738
  <Tabs defaultValue="dados" className="flex flex-col flex-1 min-h-0">
711
739
  <TabsList className="mx-3 mt-2 h-8 w-auto justify-start shrink-0 bg-muted/50">
712
740
  <TabsTrigger value="dados" className="text-xs h-7 px-2.5">
713
- Dados
741
+ {t('lessonForm.tabData')}
714
742
  </TabsTrigger>
715
743
  <TabsTrigger value="conteudo" className="text-xs h-7 px-2.5">
716
- Conteúdo
744
+ {t('lessonForm.postContent')}
717
745
  </TabsTrigger>
718
746
  <TabsTrigger value="recursos" className="text-xs h-7 px-2.5">
719
- Recursos
747
+ {t('lessonForm.tabResources')}
720
748
  </TabsTrigger>
721
749
  </TabsList>
722
750
 
@@ -728,7 +756,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
728
756
  <Card className="bg-muted/20 py-2 gap-2">
729
757
  <CardHeader className="px-3 pt-2 pb-0">
730
758
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
731
- Identificação
759
+ {t('questionEditor.identification')}
732
760
  </CardTitle>
733
761
  </CardHeader>
734
762
  <CardContent className="px-3 pb-2 flex flex-col gap-3">
@@ -738,7 +766,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
738
766
  name="code"
739
767
  render={({ field }) => (
740
768
  <FormItem>
741
- <FormLabel className="text-xs">Código</FormLabel>
769
+ <FormLabel className="text-xs">
770
+ {t('lessonForm.code')}
771
+ </FormLabel>
742
772
  <FormControl>
743
773
  <Input
744
774
  {...field}
@@ -756,7 +786,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
756
786
  <FormItem>
757
787
  <FormLabel className="text-xs">
758
788
  <Clock className="size-3 inline mr-1" />
759
- Duração (min)
789
+ {t('lessonForm.duration')}
760
790
  </FormLabel>
761
791
  <FormControl>
762
792
  <Input
@@ -774,7 +804,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
774
804
  name="type"
775
805
  render={({ field }) => (
776
806
  <FormItem>
777
- <FormLabel className="text-xs">Tipo</FormLabel>
807
+ <FormLabel className="text-xs">
808
+ {t('lessonForm.type')}
809
+ </FormLabel>
778
810
  <Select
779
811
  value={field.value}
780
812
  onValueChange={field.onChange}
@@ -798,7 +830,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
798
830
  <Ic
799
831
  className={cn('size-3', cfg.color)}
800
832
  />
801
- {cfg.label}
833
+ {t(cfg.labelKey as any)}
802
834
  </span>
803
835
  </SelectItem>
804
836
  );
@@ -816,7 +848,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
816
848
  name="title"
817
849
  render={({ field }) => (
818
850
  <FormItem>
819
- <FormLabel className="text-xs">Título</FormLabel>
851
+ <FormLabel className="text-xs">
852
+ {t('lessonForm.title')}
853
+ </FormLabel>
820
854
  <FormControl>
821
855
  <Input {...field} className="h-8 text-sm" />
822
856
  </FormControl>
@@ -831,7 +865,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
831
865
  <Card className="bg-muted/20 py-2 gap-2">
832
866
  <CardHeader className="px-3 pt-2 pb-0">
833
867
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
834
- Publicação
868
+ {t('questionEditor.publication')}
835
869
  </CardTitle>
836
870
  </CardHeader>
837
871
  <CardContent className="px-3 pb-2 flex flex-col gap-3">
@@ -842,7 +876,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
842
876
  render={({ field }) => (
843
877
  <FormItem>
844
878
  <FormLabel className="text-xs">
845
- Status de produção
879
+ {t('questionEditor.productionStatus')}
846
880
  </FormLabel>
847
881
  <Select
848
882
  value={field.value}
@@ -855,7 +889,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
855
889
  </FormControl>
856
890
  <SelectContent>
857
891
  {(
858
- Object.entries(STATUS_LABELS) as [
892
+ Object.entries(statusLabels) as [
859
893
  LessonStatus,
860
894
  string,
861
895
  ][]
@@ -867,10 +901,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
867
901
  STATUS_COLORS[val]
868
902
  )}
869
903
  >
870
- {lbl}
871
- </span>
872
- </SelectItem>
873
- ))}
904
+ {lbl}
905
+ </span>
906
+ </SelectItem>
907
+ ))}
874
908
  </SelectContent>
875
909
  </Select>
876
910
  <FormMessage className="text-xs" />
@@ -884,7 +918,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
884
918
  render={({ field }) => (
885
919
  <FormItem>
886
920
  <FormLabel className="text-xs">
887
- Visibilidade
921
+ {t('questionEditor.visibility')}
888
922
  </FormLabel>
889
923
  <Select
890
924
  value={field.value}
@@ -898,17 +932,17 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
898
932
  <SelectContent>
899
933
  <SelectItem value="publico">
900
934
  <span className="flex items-center gap-1.5">
901
- <Eye className="size-3" /> Público
935
+ <Eye className="size-3" /> {t('lessonForm.public')}
902
936
  </span>
903
937
  </SelectItem>
904
938
  <SelectItem value="privado">
905
939
  <span className="flex items-center gap-1.5">
906
- <EyeOff className="size-3" /> Privado
940
+ <EyeOff className="size-3" /> {t('lessonForm.private')}
907
941
  </span>
908
942
  </SelectItem>
909
943
  <SelectItem value="restrito">
910
944
  <span className="flex items-center gap-1.5">
911
- <Lock className="size-3" /> Restrito
945
+ <Lock className="size-3" /> {t('questionEditor.restricted')}
912
946
  </span>
913
947
  </SelectItem>
914
948
  </SelectContent>
@@ -925,7 +959,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
925
959
  <Card className="bg-muted/20 py-2 gap-2">
926
960
  <CardHeader className="px-3 pt-2 pb-0">
927
961
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
928
- Instrutores
962
+ {t('lessonForm.lessonInstructors')}
929
963
  </CardTitle>
930
964
  </CardHeader>
931
965
  <CardContent className="px-3 pb-2 flex flex-col gap-2">
@@ -940,10 +974,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
940
974
  : [...prev, String(val)]
941
975
  );
942
976
  }}
943
- placeholder="Adicionar instrutor…"
944
- searchPlaceholder="Buscar instrutor…"
945
- emptyLabel="Nenhum instrutor disponível"
946
- entityLabel="Instrutor"
977
+ placeholder={t('questionEditor.addInstructor')}
978
+ searchPlaceholder={t('questionEditor.searchInstructor')}
979
+ emptyLabel={t('questionEditor.noInstructorsAvailable')}
980
+ entityLabel={t('questionEditor.instructorEntity')}
947
981
  options={instructorPool.filter(
948
982
  (i) => !selectedInstructorIds.includes(String(i.id))
949
983
  )}
@@ -990,7 +1024,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
990
1024
  prev.filter((id) => id !== sid)
991
1025
  )
992
1026
  }
993
- aria-label={`Remover ${displayName}`}
1027
+ aria-label={t('questionEditor.removeInstructor', {
1028
+ name: displayName,
1029
+ })}
994
1030
  >
995
1031
  <X className="size-3" />
996
1032
  </Button>
@@ -1013,7 +1049,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1013
1049
  <Card className="bg-muted/20 py-2 gap-2">
1014
1050
  <CardHeader className="px-3 pt-2 pb-1">
1015
1051
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
1016
- <Eye className="size-3" /> Descrição pública
1052
+ <Eye className="size-3" /> {t('lessonForm.publicDescription')}
1017
1053
  </CardTitle>
1018
1054
  </CardHeader>
1019
1055
  <CardContent className="px-3 pb-2">
@@ -1039,7 +1075,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1039
1075
  <Card className="bg-muted/20 py-2 gap-2">
1040
1076
  <CardHeader className="px-3 pt-2 pb-1">
1041
1077
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
1042
- <Lock className="size-3" /> Notas internas
1078
+ <Lock className="size-3" /> {t('lessonForm.privateDescription')}
1043
1079
  </CardTitle>
1044
1080
  </CardHeader>
1045
1081
  <CardContent className="px-3 pb-2">
@@ -1067,7 +1103,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1067
1103
  <Card className="bg-muted/20 py-2 gap-2">
1068
1104
  <CardHeader className="px-3 pt-2 pb-1">
1069
1105
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
1070
- <Video className="size-3 text-blue-500" /> Vídeo
1106
+ <Video className="size-3 text-blue-500" /> {t('types.video')}
1071
1107
  </CardTitle>
1072
1108
  </CardHeader>
1073
1109
  <CardContent className="px-3 pb-2 flex flex-col gap-3">
@@ -1076,7 +1112,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1076
1112
  name="videoProvider"
1077
1113
  render={({ field }) => (
1078
1114
  <FormItem>
1079
- <FormLabel className="text-xs">Provider</FormLabel>
1115
+ <FormLabel className="text-xs">
1116
+ {t('lessonForm.videoProvider')}
1117
+ </FormLabel>
1080
1118
  <Select
1081
1119
  value={field.value}
1082
1120
  onValueChange={field.onChange}
@@ -1087,7 +1125,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1087
1125
  </SelectTrigger>
1088
1126
  </FormControl>
1089
1127
  <SelectContent>
1090
- {VIDEO_PROVIDERS.map((p) => (
1128
+ {videoProviders.map((p) => (
1091
1129
  <SelectItem key={p.value} value={p.value}>
1092
1130
  {p.label}
1093
1131
  </SelectItem>
@@ -1105,14 +1143,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1105
1143
  render={({ field }) => (
1106
1144
  <FormItem>
1107
1145
  <FormLabel className="text-xs">
1108
- URL do vídeo
1146
+ {t('lessonForm.videoUrl')}
1109
1147
  </FormLabel>
1110
1148
  <FormControl>
1111
1149
  <Input
1112
1150
  {...field}
1113
1151
  value={field.value ?? ''}
1114
1152
  className="h-8 text-xs font-mono"
1115
- placeholder="https://…"
1153
+ placeholder={t('lessonForm.videoUrlPlaceholder')}
1116
1154
  />
1117
1155
  </FormControl>
1118
1156
  <FormMessage className="text-xs" />
@@ -1126,7 +1164,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1126
1164
  render={({ field }) => (
1127
1165
  <FormItem>
1128
1166
  <FormLabel className="text-xs">
1129
- Transcrição
1167
+ {t('lessonForm.tabTranscription')}
1130
1168
  </FormLabel>
1131
1169
  <FormControl>
1132
1170
  <Textarea
@@ -1134,7 +1172,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1134
1172
  value={field.value ?? ''}
1135
1173
  rows={5}
1136
1174
  className="text-xs resize-none font-mono"
1137
- placeholder="Transcrição automática ou manual do vídeo…"
1175
+ placeholder={t('lessonForm.transcriptionPlaceholder')}
1138
1176
  />
1139
1177
  </FormControl>
1140
1178
  <FormMessage className="text-xs" />
@@ -1150,7 +1188,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1150
1188
  <CardHeader className="px-3 pt-2 pb-1">
1151
1189
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
1152
1190
  <FileText className="size-3 text-emerald-500" />{' '}
1153
- Conteúdo do post
1191
+ {t('lessonForm.postContent')}
1154
1192
  </CardTitle>
1155
1193
  </CardHeader>
1156
1194
  <CardContent className="px-3 pb-2">
@@ -1179,7 +1217,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1179
1217
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center justify-between gap-1.5">
1180
1218
  <span className="flex items-center gap-1.5">
1181
1219
  <ListChecks className="size-3 text-amber-500" />{' '}
1182
- Questão vinculada
1220
+ {t('lessonForm.linkedExam')}
1183
1221
  </span>
1184
1222
  <Button
1185
1223
  type="button"
@@ -1189,7 +1227,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1189
1227
  onClick={openCreateQuestion}
1190
1228
  >
1191
1229
  <Plus className="size-3" />
1192
- Nova questão
1230
+ {t('questionEditor.newQuestion')}
1193
1231
  </Button>
1194
1232
  </CardTitle>
1195
1233
  </CardHeader>
@@ -1209,15 +1247,15 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1209
1247
  null;
1210
1248
  setSelectedQuestion(found);
1211
1249
  }}
1212
- placeholder="Selecionar questão…"
1213
- searchPlaceholder="Buscar questão…"
1214
- emptyLabel="Nenhuma questão encontrada"
1215
- entityLabel="Questão"
1250
+ placeholder={t('questionEditor.selectQuestion')}
1251
+ searchPlaceholder={t('questionEditor.searchQuestion')}
1252
+ emptyLabel={t('questionEditor.noQuestionsFound')}
1253
+ entityLabel={t('questionEditor.questionEntity')}
1216
1254
  options={MOCK_QUESTIONS}
1217
1255
  getOptionValue={(o) => o.id}
1218
1256
  getOptionLabel={(o) => o.title}
1219
1257
  getOptionDescription={(o) =>
1220
- QUESTION_TYPE_LABELS[o.type]
1258
+ questionTypeLabels[o.type]
1221
1259
  }
1222
1260
  />
1223
1261
  </FormControl>
@@ -1233,9 +1271,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1233
1271
  {selectedQuestion.title}
1234
1272
  </p>
1235
1273
  <p className="text-xs text-muted-foreground">
1236
- {QUESTION_TYPE_LABELS[selectedQuestion.type]}
1274
+ {questionTypeLabels[selectedQuestion.type]}
1237
1275
  {selectedQuestion.points != null &&
1238
- ` · ${selectedQuestion.points} pt`}
1276
+ ` · ${selectedQuestion.points} ${t('questionEditor.pointsSuffix')}`}
1239
1277
  </p>
1240
1278
  </div>
1241
1279
  <Button
@@ -1244,7 +1282,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1244
1282
  size="icon"
1245
1283
  className="size-7 shrink-0"
1246
1284
  onClick={() => openEditQuestion(selectedQuestion)}
1247
- aria-label="Editar questão"
1285
+ aria-label={t('questionEditor.editQuestion')}
1248
1286
  >
1249
1287
  <Pencil className="size-3.5" />
1250
1288
  </Button>
@@ -1313,11 +1351,13 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1313
1351
  )}
1314
1352
  <div className="text-center">
1315
1353
  <p className="text-xs font-medium">
1316
- {isUploading ? 'Enviando…' : 'Solte o arquivo aqui'}
1354
+ {isUploading
1355
+ ? t('questionEditor.uploading')
1356
+ : t('questionEditor.dropFile')}
1317
1357
  </p>
1318
1358
  {!isUploading && (
1319
1359
  <p className="text-xs text-muted-foreground">
1320
- ou clique para selecionar
1360
+ {t('questionEditor.clickToSelect')}
1321
1361
  </p>
1322
1362
  )}
1323
1363
  </div>
@@ -1338,15 +1378,16 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1338
1378
  {/* Counter */}
1339
1379
  {localResources.length > 0 && (
1340
1380
  <p className="text-xs text-muted-foreground">
1341
- {localResources.length} recurso
1342
- {localResources.length !== 1 ? 's' : ''}
1381
+ {t('lessonForm.resourcesCount', {
1382
+ count: localResources.length,
1383
+ })}
1343
1384
  </p>
1344
1385
  )}
1345
1386
 
1346
1387
  {/* Resource list */}
1347
1388
  {localResources.length === 0 ? (
1348
1389
  <p className="text-center text-xs text-muted-foreground py-1">
1349
- Nenhum recurso vinculado.
1390
+ {t('questionEditor.noLinkedResources')}
1350
1391
  </p>
1351
1392
  ) : (
1352
1393
  <div className="flex flex-col gap-1">
@@ -1374,7 +1415,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1374
1415
  {res.public && (
1375
1416
  <Eye
1376
1417
  className="size-3 text-emerald-500 shrink-0"
1377
- aria-label="Público"
1418
+ aria-label={t('lessonForm.public')}
1378
1419
  />
1379
1420
  )}
1380
1421
  {/* Abrir em nova aba */}
@@ -1383,7 +1424,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1383
1424
  href={res.url}
1384
1425
  target="_blank"
1385
1426
  rel="noopener noreferrer"
1386
- aria-label={`Abrir ${res.name} em nova aba`}
1427
+ aria-label={t('questionEditor.openInNewTab', {
1428
+ name: res.name,
1429
+ })}
1387
1430
  onClick={(e) => e.stopPropagation()}
1388
1431
  className="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors shrink-0"
1389
1432
  >
@@ -1397,7 +1440,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1397
1440
  size="icon"
1398
1441
  className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
1399
1442
  onClick={() => handleResourceDownload(res)}
1400
- aria-label={`Baixar ${res.name}`}
1443
+ aria-label={t('questionEditor.downloadResource', {
1444
+ name: res.name,
1445
+ })}
1401
1446
  >
1402
1447
  <Download className="size-3" />
1403
1448
  </Button>
@@ -1408,7 +1453,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1408
1453
  size="icon"
1409
1454
  className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
1410
1455
  onClick={() => void removeResource(res.id)}
1411
- aria-label={`Remover ${res.name}`}
1456
+ aria-label={t('questionEditor.removeResource', {
1457
+ name: res.name,
1458
+ })}
1412
1459
  >
1413
1460
  <X className="size-3" />
1414
1461
  </Button>
@@ -1435,7 +1482,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1435
1482
  onClick={() => form.reset()}
1436
1483
  >
1437
1484
  <Undo2 className="size-3 mr-1" />
1438
- Cancelar
1485
+ {t('lessonForm.cancel')}
1439
1486
  </Button>
1440
1487
  <div className="flex-1" />
1441
1488
  <Button
@@ -1449,7 +1496,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1449
1496
  ) : (
1450
1497
  <Save className="size-3 mr-1" />
1451
1498
  )}
1452
- Salvar aula
1499
+ {t('lessonForm.save')}
1453
1500
  </Button>
1454
1501
  </div>
1455
1502
  </div>
@@ -1462,14 +1509,18 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1462
1509
  >
1463
1510
  <SheetHeader>
1464
1511
  <SheetTitle>
1465
- {editingQuestion ? 'Editar questão' : 'Nova questão'}
1512
+ {editingQuestion
1513
+ ? t('questionEditor.editQuestion')
1514
+ : t('questionEditor.newQuestion')}
1466
1515
  </SheetTitle>
1467
1516
  </SheetHeader>
1468
1517
 
1469
1518
  <div className="flex flex-1 flex-col gap-5 px-4 pb-4">
1470
1519
  {/* Enunciado */}
1471
1520
  <div className="flex flex-col gap-1.5">
1472
- <Label className="text-sm font-medium">Enunciado</Label>
1521
+ <Label className="text-sm font-medium">
1522
+ {t('questionEditor.statement')}
1523
+ </Label>
1473
1524
  <RichTextEditor
1474
1525
  value={qSheetStatement}
1475
1526
  onChange={setQSheetStatement}
@@ -1484,7 +1535,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1484
1535
  {/* Tipo + Pontuação */}
1485
1536
  <div className="grid grid-cols-2 gap-3">
1486
1537
  <div className="flex flex-col gap-1.5">
1487
- <Label className="text-sm font-medium">Tipo</Label>
1538
+ <Label className="text-sm font-medium">
1539
+ {t('questionEditor.type')}
1540
+ </Label>
1488
1541
  <Select
1489
1542
  value={qSheetType}
1490
1543
  onValueChange={(v) =>
@@ -1496,17 +1549,19 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1496
1549
  </SelectTrigger>
1497
1550
  <SelectContent>
1498
1551
  {(
1499
- Object.keys(QUESTION_TYPE_LABELS) as QuestionType[]
1552
+ Object.keys(questionTypeLabels) as QuestionType[]
1500
1553
  ).map((t) => (
1501
1554
  <SelectItem key={t} value={t}>
1502
- {QUESTION_TYPE_LABELS[t]}
1555
+ {questionTypeLabels[t]}
1503
1556
  </SelectItem>
1504
1557
  ))}
1505
1558
  </SelectContent>
1506
1559
  </Select>
1507
1560
  </div>
1508
1561
  <div className="flex flex-col gap-1.5">
1509
- <Label className="text-sm font-medium">Pontuação</Label>
1562
+ <Label className="text-sm font-medium">
1563
+ {t('questionEditor.points')}
1564
+ </Label>
1510
1565
  <Input
1511
1566
  type="number"
1512
1567
  min={0}
@@ -1523,7 +1578,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1523
1578
  {qSheetType === 'multiple_choice' && (
1524
1579
  <div className="flex flex-col gap-2">
1525
1580
  <div className="flex items-center justify-between">
1526
- <Label className="text-sm font-medium">Alternativas</Label>
1581
+ <Label className="text-sm font-medium">
1582
+ {t('questionEditor.alternatives')}
1583
+ </Label>
1527
1584
  <Button
1528
1585
  type="button"
1529
1586
  variant="outline"
@@ -1537,7 +1594,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1537
1594
  }
1538
1595
  >
1539
1596
  <Plus className="size-3" />
1540
- Adicionar
1597
+ {t('questionEditor.add')}
1541
1598
  </Button>
1542
1599
  </div>
1543
1600
  {qSheetErrors.alts && (
@@ -1569,6 +1626,15 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1569
1626
  key={alt.id}
1570
1627
  alt={alt}
1571
1628
  index={idx}
1629
+ dragLabel={t('questionEditor.dragAlternative')}
1630
+ textPlaceholder={t(
1631
+ 'questionEditor.alternativeTextPlaceholder'
1632
+ )}
1633
+ markIncorrectLabel={t(
1634
+ 'questionEditor.markAsIncorrect'
1635
+ )}
1636
+ markCorrectLabel={t('questionEditor.markAsCorrect')}
1637
+ removeLabel={t('questionEditor.removeAlternative')}
1572
1638
  canRemove={qSheetAlts.length > 2}
1573
1639
  onToggleCorrect={() =>
1574
1640
  setQSheetAlts((prev) =>
@@ -1602,7 +1668,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1602
1668
  {qSheetType === 'true_false' && (
1603
1669
  <div className="flex flex-col gap-2">
1604
1670
  <Label className="text-sm font-medium">
1605
- Resposta correta
1671
+ {t('questionEditor.correctAnswer')}
1606
1672
  </Label>
1607
1673
  {qSheetErrors.alts && (
1608
1674
  <p className="text-xs text-destructive">
@@ -1642,7 +1708,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1642
1708
  {qSheetType === 'fill_blank' && (
1643
1709
  <div className="flex flex-col gap-2">
1644
1710
  <div className="flex items-center justify-between">
1645
- <Label className="text-sm font-medium">Lacunas</Label>
1711
+ <Label className="text-sm font-medium">
1712
+ {t('questionEditor.blanks')}
1713
+ </Label>
1646
1714
  <Button
1647
1715
  type="button"
1648
1716
  variant="outline"
@@ -1660,7 +1728,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1660
1728
  }
1661
1729
  >
1662
1730
  <Plus className="size-3" />
1663
- Adicionar
1731
+ {t('questionEditor.add')}
1664
1732
  </Button>
1665
1733
  </div>
1666
1734
  {qSheetFillBlanks.map((fb, idx) => (
@@ -1670,7 +1738,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1670
1738
  >
1671
1739
  <div className="flex items-center justify-between">
1672
1740
  <span className="text-xs font-medium text-muted-foreground">
1673
- Lacuna {idx + 1}
1741
+ {t('questionEditor.blankLabel', {
1742
+ count: idx + 1,
1743
+ })}
1674
1744
  </span>
1675
1745
  {qSheetFillBlanks.length > 1 && (
1676
1746
  <button
@@ -1681,14 +1751,16 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1681
1751
  prev.filter((f) => f.id !== fb.id)
1682
1752
  )
1683
1753
  }
1684
- aria-label="Remover lacuna"
1754
+ aria-label={t('questionEditor.removeBlank')}
1685
1755
  >
1686
1756
  <X className="size-3.5" />
1687
1757
  </button>
1688
1758
  )}
1689
1759
  </div>
1690
1760
  <Input
1691
- placeholder="Resposta correta"
1761
+ placeholder={t(
1762
+ 'questionEditor.correctAnswerPlaceholder'
1763
+ )}
1692
1764
  value={fb.answer}
1693
1765
  onChange={(e) =>
1694
1766
  setQSheetFillBlanks((prev) =>
@@ -1701,7 +1773,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1701
1773
  }
1702
1774
  />
1703
1775
  <Input
1704
- placeholder="Alternativas separadas por vírgula (opcional)"
1776
+ placeholder={t(
1777
+ 'questionEditor.alternativesPlaceholder'
1778
+ )}
1705
1779
  value={fb.alternativesText}
1706
1780
  onChange={(e) =>
1707
1781
  setQSheetFillBlanks((prev) =>
@@ -1723,7 +1797,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1723
1797
  <div className="flex flex-col gap-2">
1724
1798
  <div className="flex items-center justify-between">
1725
1799
  <Label className="text-sm font-medium">
1726
- Pares de associação
1800
+ {t('questionEditor.matchingPairs')}
1727
1801
  </Label>
1728
1802
  <Button
1729
1803
  type="button"
@@ -1738,7 +1812,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1738
1812
  }
1739
1813
  >
1740
1814
  <Plus className="size-3" />
1741
- Adicionar
1815
+ {t('questionEditor.add')}
1742
1816
  </Button>
1743
1817
  </div>
1744
1818
  {qSheetPairs.map((pair, idx) => (
@@ -1747,7 +1821,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1747
1821
  {idx + 1}
1748
1822
  </span>
1749
1823
  <Input
1750
- placeholder="Esquerda"
1824
+ placeholder={t('questionEditor.left')}
1751
1825
  value={pair.leftText}
1752
1826
  onChange={(e) =>
1753
1827
  setQSheetPairs((prev) =>
@@ -1762,7 +1836,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1762
1836
  />
1763
1837
  <span className="text-muted-foreground text-xs">↔</span>
1764
1838
  <Input
1765
- placeholder="Direita"
1839
+ placeholder={t('questionEditor.right')}
1766
1840
  value={pair.rightText}
1767
1841
  onChange={(e) =>
1768
1842
  setQSheetPairs((prev) =>
@@ -1784,7 +1858,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1784
1858
  prev.filter((p) => p.id !== pair.id)
1785
1859
  )
1786
1860
  }
1787
- aria-label="Remover par"
1861
+ aria-label={t('questionEditor.removePair')}
1788
1862
  >
1789
1863
  <X className="size-4" />
1790
1864
  </button>
@@ -1797,7 +1871,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1797
1871
  {/* Dissertativa */}
1798
1872
  {qSheetType === 'essay' && (
1799
1873
  <div className="rounded-lg border bg-muted/30 p-4 text-center text-sm text-muted-foreground">
1800
- Questão dissertativa — o aluno responderá em texto livre.
1874
+ {t('questionEditor.essayHelp')}
1801
1875
  </div>
1802
1876
  )}
1803
1877
  </div>
@@ -1809,11 +1883,13 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1809
1883
  className="flex-1"
1810
1884
  onClick={() => setQuestionSheetOpen(false)}
1811
1885
  >
1812
- Cancelar
1886
+ {t('lessonForm.cancel')}
1813
1887
  </Button>
1814
1888
  <Button type="button" className="flex-1" onClick={saveQuestion}>
1815
1889
  <Save className="size-4 mr-1.5" />
1816
- {editingQuestion ? 'Salvar alterações' : 'Criar questão'}
1890
+ {editingQuestion
1891
+ ? t('questionEditor.saveChanges')
1892
+ : t('questionEditor.createQuestion')}
1817
1893
  </Button>
1818
1894
  </SheetFooter>
1819
1895
  </SheetContent>