Enhance localization support by integrating i18next for translations across various components and pages. Update UI elements to utilize translated strings for improved user experience in both English and Russian. Additionally, refactor the Toaster component to support a context-based approach for toast notifications.

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-11-04 10:25:12 +03:00
parent daa44521b9
commit 44a7ac2bfd
19 changed files with 892 additions and 293 deletions
+41 -35
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import {
Box,
Heading,
@@ -9,8 +10,6 @@ import {
VStack,
HStack,
Text,
Flex,
Stack,
Field,
Tabs,
} from '@chakra-ui/react'
@@ -29,7 +28,7 @@ export const TaskFormPage: React.FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const isEdit = !!id
const { t } = useTranslation()
const { data: task, isLoading: isLoadingTask, error: loadError } = useGetTaskQuery(id!, {
skip: !id,
})
@@ -54,8 +53,8 @@ export const TaskFormPage: React.FC = () => {
if (!title.trim() || !description.trim()) {
toaster.create({
title: 'Ошибка валидации',
description: 'Заполните обязательные поля',
title: t('challenge.admin.common.validation.error'),
description: t('challenge.admin.tasks.validation.fill.required.fields'),
type: 'error',
})
return
@@ -72,8 +71,8 @@ export const TaskFormPage: React.FC = () => {
},
}).unwrap()
toaster.create({
title: 'Успешно',
description: 'Задание обновлено',
title: t('challenge.admin.common.success'),
description: t('challenge.admin.tasks.updated'),
type: 'success',
})
} else {
@@ -83,34 +82,42 @@ export const TaskFormPage: React.FC = () => {
hiddenInstructions: hiddenInstructions.trim() || undefined,
}).unwrap()
toaster.create({
title: 'Успешно',
description: 'Задание создано',
title: t('challenge.admin.common.success'),
description: t('challenge.admin.tasks.created'),
type: 'success',
})
}
navigate(URLs.tasks)
} catch (err: any) {
} catch (err: unknown) {
const errorMessage =
(err && typeof err === 'object' && 'data' in err &&
err.data && typeof err.data === 'object' && 'error' in err.data &&
err.data.error && typeof err.data.error === 'object' && 'message' in err.data.error &&
typeof err.data.error.message === 'string')
? err.data.error.message
: t('challenge.admin.tasks.save.error')
toaster.create({
title: 'Ошибка',
description: err?.data?.error?.message || 'Не удалось сохранить задание',
title: t('challenge.admin.common.error'),
description: errorMessage,
type: 'error',
})
}
}
if (isEdit && isLoadingTask) {
return <LoadingSpinner message="Загрузка задания..." />
return <LoadingSpinner message={t('challenge.admin.tasks.loading')} />
}
if (isEdit && loadError) {
return <ErrorAlert message="Не удалось загрузить задание" />
return <ErrorAlert message={t('challenge.admin.tasks.load.error')} />
}
const isLoading = isCreating || isUpdating
return (
<Box>
<Heading mb={6}>{isEdit ? 'Редактировать задание' : 'Создать задание'}</Heading>
<Heading mb={6}>{isEdit ? t('challenge.admin.tasks.edit.title') : t('challenge.admin.tasks.create.title')}</Heading>
<Box
as="form"
@@ -125,33 +132,33 @@ export const TaskFormPage: React.FC = () => {
<VStack gap={6} align="stretch">
{/* Title */}
<Field.Root required>
<Field.Label>Название задания</Field.Label>
<Field.Label>{t('challenge.admin.tasks.field.title')}</Field.Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Введите название задания"
placeholder={t('challenge.admin.tasks.field.title.placeholder')}
maxLength={255}
disabled={isLoading}
/>
<Field.HelperText>Максимум 255 символов</Field.HelperText>
<Field.HelperText>{t('challenge.admin.tasks.field.title.helper')}</Field.HelperText>
</Field.Root>
{/* Description with Markdown */}
<Field.Root required>
<Field.Label>Описание (Markdown)</Field.Label>
<Field.Label>{t('challenge.admin.tasks.field.description')}</Field.Label>
<Tabs.Root
value={showDescPreview ? 'preview' : 'editor'}
onValueChange={(e) => setShowDescPreview(e.value === 'preview')}
>
<Tabs.List>
<Tabs.Trigger value="editor">Редактор</Tabs.Trigger>
<Tabs.Trigger value="preview">Превью</Tabs.Trigger>
<Tabs.Trigger value="editor">{t('challenge.admin.tasks.tab.editor')}</Tabs.Trigger>
<Tabs.Trigger value="preview">{t('challenge.admin.tasks.tab.preview')}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="editor" pt={4}>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="# Заголовок задания&#10;&#10;Описание задания в формате Markdown..."
placeholder={t('challenge.admin.tasks.field.description.placeholder')}
rows={15}
fontFamily="monospace"
disabled={isLoading}
@@ -172,13 +179,13 @@ export const TaskFormPage: React.FC = () => {
</Box>
) : (
<Text color="gray.400" fontStyle="italic">
Предпросмотр появится здесь...
{t('challenge.admin.tasks.preview.empty')}
</Text>
)}
</Box>
</Tabs.Content>
</Tabs.Root>
<Field.HelperText>Используйте Markdown для форматирования текста</Field.HelperText>
<Field.HelperText>{t('challenge.admin.tasks.field.description.helper')}</Field.HelperText>
</Field.Root>
{/* Hidden Instructions */}
@@ -186,22 +193,21 @@ export const TaskFormPage: React.FC = () => {
<Box bg="purple.50" p={4} borderRadius="md" borderWidth="1px" borderColor="purple.200">
<HStack mb={2}>
<Text fontWeight="bold" color="purple.800">
🔒 Скрытые инструкции для LLM
{t('challenge.admin.tasks.field.hidden.instructions')}
</Text>
</HStack>
<Text fontSize="sm" color="purple.700" mb={3}>
Эти инструкции будут переданы LLM при проверке решений студентов. Студенты их не
увидят.
{t('challenge.admin.tasks.field.hidden.instructions.description')}
</Text>
<Textarea
value={hiddenInstructions}
onChange={(e) => setHiddenInstructions(e.target.value)}
placeholder="Например: Проверь, что сложность алгоритма O(n log n). Код должен обрабатывать edge cases..."
placeholder={t('challenge.admin.tasks.field.hidden.instructions.placeholder')}
rows={6}
disabled={isLoading}
/>
<Field.HelperText>
Опционально. Используйте для тонкой настройки проверки LLM.
{t('challenge.admin.tasks.field.hidden.instructions.helper')}
</Field.HelperText>
</Box>
</Field.Root>
@@ -210,17 +216,17 @@ export const TaskFormPage: React.FC = () => {
{isEdit && task && (
<Box p={4} bg="gray.50" borderRadius="md">
<Text fontSize="sm" color="gray.600">
<strong>Создано:</strong>{' '}
<strong>{t('challenge.admin.tasks.meta.created')}</strong>{' '}
{new Date(task.createdAt).toLocaleString('ru-RU')}
</Text>
{task.creator && (
<Text fontSize="sm" color="gray.600">
<strong>Автор:</strong> {task.creator.preferred_username}
<strong>{t('challenge.admin.tasks.meta.author')}</strong> {task.creator.preferred_username}
</Text>
)}
{task.updatedAt !== task.createdAt && (
<Text fontSize="sm" color="gray.600">
<strong>Обновлено:</strong>{' '}
<strong>{t('challenge.admin.tasks.meta.updated')}</strong>{' '}
{new Date(task.updatedAt).toLocaleString('ru-RU')}
</Text>
)}
@@ -230,10 +236,10 @@ export const TaskFormPage: React.FC = () => {
{/* Actions */}
<HStack gap={3} justify="flex-end">
<Button variant="outline" onClick={() => navigate(URLs.tasks)} disabled={isLoading}>
Отмена
{t('challenge.admin.common.cancel')}
</Button>
<Button type="submit" colorPalette="teal" loading={isLoading}>
{isEdit ? 'Сохранить изменения' : 'Создать задание'}
<Button type="submit" colorPalette="teal" disabled={isLoading}>
{isEdit ? t('challenge.admin.tasks.button.save') : t('challenge.admin.tasks.button.create')}
</Button>
</HStack>
</VStack>