diff --git a/locales/en.json b/locales/en.json index 0487407..31cd851 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,3 +1,192 @@ { - "challenge.title": "Challenge" + "challenge.title": "Challenge", + "challenge.admin.common.success": "Success", + "challenge.admin.common.error": "Error", + "challenge.admin.common.cancel": "Cancel", + "challenge.admin.common.loading.tasks": "Loading tasks...", + "challenge.admin.common.not.found": "Nothing found", + "challenge.admin.common.validation.error": "Validation error", + "challenge.admin.tasks.updated": "Task updated", + "challenge.admin.tasks.created": "Task created", + "challenge.admin.tasks.validation.fill.required.fields": "Fill in required fields", + "challenge.admin.tasks.save.error": "Failed to save task", + "challenge.admin.tasks.loading": "Loading task...", + "challenge.admin.tasks.load.error": "Failed to load task", + "challenge.admin.tasks.edit.title": "Edit task", + "challenge.admin.tasks.create.title": "Create task", + "challenge.admin.tasks.field.title": "Task title", + "challenge.admin.tasks.field.title.placeholder": "Enter task title", + "challenge.admin.tasks.field.title.helper": "Maximum 255 characters", + "challenge.admin.tasks.field.description": "Description (Markdown)", + "challenge.admin.tasks.field.description.placeholder": "# Task title\n\nTask description in Markdown format...", + "challenge.admin.tasks.field.description.helper": "Use Markdown to format text", + "challenge.admin.tasks.tab.editor": "Editor", + "challenge.admin.tasks.tab.preview": "Preview", + "challenge.admin.tasks.preview.empty": "Preview will appear here...", + "challenge.admin.tasks.field.hidden.instructions": "🔒 Hidden instructions for LLM", + "challenge.admin.tasks.field.hidden.instructions.description": "These instructions will be passed to the LLM when checking student solutions. Students will not see them.", + "challenge.admin.tasks.field.hidden.instructions.placeholder": "Example: Check that algorithm complexity is O(n log n). Code should handle edge cases...", + "challenge.admin.tasks.field.hidden.instructions.helper": "Optional. Use for fine-tuning LLM verification.", + "challenge.admin.tasks.meta.created": "Created:", + "challenge.admin.tasks.meta.author": "Author:", + "challenge.admin.tasks.meta.updated": "Updated:", + "challenge.admin.tasks.button.save": "Save changes", + "challenge.admin.tasks.button.create": "Create task", + "challenge.admin.tasks.list.title": "Tasks", + "challenge.admin.tasks.list.create.button": "+ Create Task", + "challenge.admin.tasks.list.search.placeholder": "Search by name...", + "challenge.admin.tasks.list.empty.title": "No tasks", + "challenge.admin.tasks.list.empty.description": "Create your first task to get started", + "challenge.admin.tasks.list.empty.action": "Create task", + "challenge.admin.tasks.list.search.empty": "Nothing found for \"{query}\"", + "challenge.admin.tasks.list.table.title": "Title", + "challenge.admin.tasks.list.table.creator": "Creator", + "challenge.admin.tasks.list.table.created": "Created date", + "challenge.admin.tasks.list.table.hidden.instructions": "Hidden instructions", + "challenge.admin.tasks.list.table.actions": "Actions", + "challenge.admin.tasks.list.badge.has.instructions": "🔒 Yes", + "challenge.admin.tasks.list.button.edit": "Edit", + "challenge.admin.tasks.list.button.delete": "Delete", + "challenge.admin.tasks.deleted": "Task deleted", + "challenge.admin.tasks.delete.error": "Failed to delete task", + "challenge.admin.tasks.list.loading": "Loading tasks...", + "challenge.admin.tasks.list.load.error": "Failed to load tasks list", + "challenge.admin.tasks.delete.confirm.title": "Delete task", + "challenge.admin.tasks.delete.confirm.message": "Are you sure you want to delete task \"{title}\"? This action cannot be undone.", + "challenge.admin.tasks.delete.confirm.button": "Delete", + "challenge.admin.chains.updated": "Chain updated", + "challenge.admin.chains.created": "Chain created", + "challenge.admin.chains.validation.enter.name": "Enter chain name", + "challenge.admin.chains.validation.add.task": "Add at least one task", + "challenge.admin.chains.save.error": "Failed to save chain", + "challenge.admin.chains.loading": "Loading chain...", + "challenge.admin.chains.load.error": "Failed to load chain", + "challenge.admin.chains.tasks.load.error": "Failed to load task list", + "challenge.admin.chains.edit.title": "Edit chain", + "challenge.admin.chains.create.title": "Create chain", + "challenge.admin.chains.field.name": "Chain name", + "challenge.admin.chains.field.name.placeholder": "Enter chain name", + "challenge.admin.chains.selected.tasks": "Tasks in chain", + "challenge.admin.chains.selected.tasks.empty": "Add tasks from the list below", + "challenge.admin.chains.available.tasks": "Available tasks", + "challenge.admin.chains.search.placeholder": "Search tasks...", + "challenge.admin.chains.all.tasks.added": "All tasks already added", + "challenge.admin.chains.button.add": "+ Add", + "challenge.admin.chains.button.save": "Save changes", + "challenge.admin.chains.button.create": "Create chain", + "challenge.admin.chains.list.title": "Task Chains", + "challenge.admin.chains.list.create.button": "+ Create Chain", + "challenge.admin.chains.list.search.placeholder": "Search by name...", + "challenge.admin.chains.list.empty.title": "No chains", + "challenge.admin.chains.list.empty.description": "Create your first task chain", + "challenge.admin.chains.list.empty.action": "Create chain", + "challenge.admin.chains.list.search.empty": "Nothing found for \"{query}\"", + "challenge.admin.chains.list.table.name": "Name", + "challenge.admin.chains.list.table.tasks.count": "Number of tasks", + "challenge.admin.chains.list.table.created": "Created date", + "challenge.admin.chains.list.table.actions": "Actions", + "challenge.admin.chains.list.badge.tasks": "tasks", + "challenge.admin.chains.list.button.edit": "Edit", + "challenge.admin.chains.list.button.delete": "Delete", + "challenge.admin.chains.deleted": "Chain deleted", + "challenge.admin.chains.delete.error": "Failed to delete chain", + "challenge.admin.chains.list.loading": "Loading chains...", + "challenge.admin.chains.list.load.error": "Failed to load chains list", + "challenge.admin.chains.delete.confirm.title": "Delete chain", + "challenge.admin.chains.delete.confirm.message": "Are you sure you want to delete chain \"{name}\"? This action cannot be undone.", + "challenge.admin.chains.delete.confirm.button": "Delete", + "challenge.admin.dashboard.title": "Dashboard", + "challenge.admin.dashboard.loading": "Loading statistics...", + "challenge.admin.dashboard.load.error": "Failed to load system statistics", + "challenge.admin.dashboard.stats.users": "Total users", + "challenge.admin.dashboard.stats.tasks": "Total tasks", + "challenge.admin.dashboard.stats.chains": "Total chains", + "challenge.admin.dashboard.stats.submissions": "Total checks", + "challenge.admin.dashboard.submissions.title": "Check statistics", + "challenge.admin.dashboard.submissions.accepted": "Accepted", + "challenge.admin.dashboard.submissions.rejected": "Rejected", + "challenge.admin.dashboard.submissions.pending": "Pending", + "challenge.admin.dashboard.submissions.in.progress": "In progress", + "challenge.admin.dashboard.queue.title": "Queue status", + "challenge.admin.dashboard.queue.processing": "Processing", + "challenge.admin.dashboard.queue.waiting": "Waiting in queue", + "challenge.admin.dashboard.queue.total": "Total in queue", + "challenge.admin.dashboard.queue.utilization": "Queue utilization:", + "challenge.admin.dashboard.check.time.title": "Average check time", + "challenge.admin.dashboard.check.time.value": "{{time}} sec", + "challenge.admin.dashboard.check.time.description": "Time from solution submission to result", + "challenge.admin.users.title": "Users", + "challenge.admin.users.loading": "Loading users...", + "challenge.admin.users.load.error": "Failed to load users list", + "challenge.admin.users.search.placeholder": "Search by nickname...", + "challenge.admin.users.empty.title": "No users", + "challenge.admin.users.empty.description": "Users will appear after registration", + "challenge.admin.users.search.empty": "Nothing found for \"{query}\"", + "challenge.admin.users.table.nickname": "Nickname", + "challenge.admin.users.table.id": "ID", + "challenge.admin.users.table.registered": "Registration date", + "challenge.admin.users.table.actions": "Actions", + "challenge.admin.users.button.stats": "Statistics", + "challenge.admin.users.stats.title": "User statistics", + "challenge.admin.users.stats.loading": "Loading statistics...", + "challenge.admin.users.stats.no.data": "No data", + "challenge.admin.users.stats.completed": "Completed", + "challenge.admin.users.stats.total.submissions": "Total attempts", + "challenge.admin.users.stats.in.progress": "In progress", + "challenge.admin.users.stats.needs.revision": "Needs revision", + "challenge.admin.users.stats.chains.progress": "Chain progress", + "challenge.admin.users.stats.tasks": "Tasks", + "challenge.admin.users.stats.status.completed": "Completed", + "challenge.admin.users.stats.status.needs_revision": "Revision", + "challenge.admin.users.stats.status.in_progress": "In progress", + "challenge.admin.users.stats.status.not_started": "Not started", + "challenge.admin.users.stats.attempts": "Attempts:", + "challenge.admin.users.stats.avg.check.time": "Average check time", + "challenge.admin.users.stats.close": "Close", + "challenge.admin.submissions.title": "Solution attempts", + "challenge.admin.submissions.loading": "Loading attempts...", + "challenge.admin.submissions.load.error": "Failed to load attempts list", + "challenge.admin.submissions.search.placeholder": "Search by user or task...", + "challenge.admin.submissions.filter.status": "Status", + "challenge.admin.submissions.status.all": "All statuses", + "challenge.admin.submissions.status.accepted": "Accepted", + "challenge.admin.submissions.status.needs_revision": "Needs revision", + "challenge.admin.submissions.status.in_progress": "Checking", + "challenge.admin.submissions.status.pending": "Pending", + "challenge.admin.submissions.empty.title": "No attempts", + "challenge.admin.submissions.empty.description": "Attempts will appear after solution submissions", + "challenge.admin.submissions.search.empty.title": "Nothing found", + "challenge.admin.submissions.search.empty.description": "Try changing filters", + "challenge.admin.submissions.table.user": "User", + "challenge.admin.submissions.table.task": "Task", + "challenge.admin.submissions.table.status": "Status", + "challenge.admin.submissions.table.attempt": "Attempt", + "challenge.admin.submissions.table.submitted": "Submitted date", + "challenge.admin.submissions.table.check.time": "Check time", + "challenge.admin.submissions.table.actions": "Actions", + "challenge.admin.submissions.button.details": "Details", + "challenge.admin.submissions.check.time": "{{time}} sec", + "challenge.admin.submissions.details.title": "Attempt details", + "challenge.admin.submissions.details.user": "User", + "challenge.admin.submissions.details.status": "Status", + "challenge.admin.submissions.details.submitted": "Submitted:", + "challenge.admin.submissions.details.checked": "Checked:", + "challenge.admin.submissions.details.check.time": "Check time:", + "challenge.admin.submissions.details.task": "Task:", + "challenge.admin.submissions.details.solution": "User solution:", + "challenge.admin.submissions.details.feedback": "LLM feedback:", + "challenge.admin.submissions.details.close": "Close", + "challenge.admin.layout.title": "Challenge Admin", + "challenge.admin.layout.nav.dashboard": "Dashboard", + "challenge.admin.layout.nav.tasks": "Tasks", + "challenge.admin.layout.nav.chains": "Chains", + "challenge.admin.layout.nav.users": "Users", + "challenge.admin.layout.nav.submissions": "Attempts", + "challenge.admin.layout.button.player": "Open Player", + "challenge.admin.layout.button.logout": "Logout", + "challenge.admin.common.loading.default": "Loading...", + "challenge.admin.common.error.default": "An error occurred while loading data", + "challenge.admin.common.retry": "Try again", + "challenge.admin.common.confirm": "Confirm", + "challenge.admin.common.close": "Close" } \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 0487407..b2cbe4e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,3 +1,191 @@ { - "challenge.title": "Challenge" + "challenge.admin.common.success": "Успешно", + "challenge.admin.common.error": "Ошибка", + "challenge.admin.common.cancel": "Отмена", + "challenge.admin.common.loading.tasks": "Загрузка заданий...", + "challenge.admin.common.not.found": "Ничего не найдено", + "challenge.admin.common.validation.error": "Ошибка валидации", + "challenge.admin.tasks.updated": "Задание обновлено", + "challenge.admin.tasks.created": "Задание создано", + "challenge.admin.tasks.validation.fill.required.fields": "Заполните обязательные поля", + "challenge.admin.tasks.save.error": "Не удалось сохранить задание", + "challenge.admin.tasks.loading": "Загрузка задания...", + "challenge.admin.tasks.load.error": "Не удалось загрузить задание", + "challenge.admin.tasks.edit.title": "Редактировать задание", + "challenge.admin.tasks.create.title": "Создать задание", + "challenge.admin.tasks.field.title": "Название задания", + "challenge.admin.tasks.field.title.placeholder": "Введите название задания", + "challenge.admin.tasks.field.title.helper": "Максимум 255 символов", + "challenge.admin.tasks.field.description": "Описание (Markdown)", + "challenge.admin.tasks.field.description.placeholder": "# Заголовок задания\n\nОписание задания в формате Markdown...", + "challenge.admin.tasks.field.description.helper": "Используйте Markdown для форматирования текста", + "challenge.admin.tasks.tab.editor": "Редактор", + "challenge.admin.tasks.tab.preview": "Превью", + "challenge.admin.tasks.preview.empty": "Предпросмотр появится здесь...", + "challenge.admin.tasks.field.hidden.instructions": "🔒 Скрытые инструкции для LLM", + "challenge.admin.tasks.field.hidden.instructions.description": "Эти инструкции будут переданы LLM при проверке решений студентов. Студенты их не увидят.", + "challenge.admin.tasks.field.hidden.instructions.placeholder": "Например: Проверь, что сложность алгоритма O(n log n). Код должен обрабатывать edge cases...", + "challenge.admin.tasks.field.hidden.instructions.helper": "Опционально. Используйте для тонкой настройки проверки LLM.", + "challenge.admin.tasks.meta.created": "Создано:", + "challenge.admin.tasks.meta.author": "Автор:", + "challenge.admin.tasks.meta.updated": "Обновлено:", + "challenge.admin.tasks.button.save": "Сохранить изменения", + "challenge.admin.tasks.button.create": "Создать задание", + "challenge.admin.tasks.list.title": "Задания", + "challenge.admin.tasks.list.create.button": "+ Создать задание", + "challenge.admin.tasks.list.search.placeholder": "Поиск по названию...", + "challenge.admin.tasks.list.empty.title": "Нет заданий", + "challenge.admin.tasks.list.empty.description": "Создайте первое задание для начала работы", + "challenge.admin.tasks.list.empty.action": "Создать задание", + "challenge.admin.tasks.list.search.empty": "По запросу \"{query}\" ничего не найдено", + "challenge.admin.tasks.list.table.title": "Название", + "challenge.admin.tasks.list.table.creator": "Создатель", + "challenge.admin.tasks.list.table.created": "Дата создания", + "challenge.admin.tasks.list.table.hidden.instructions": "Скрытые инструкции", + "challenge.admin.tasks.list.table.actions": "Действия", + "challenge.admin.tasks.list.badge.has.instructions": "🔒 Есть", + "challenge.admin.tasks.list.button.edit": "Редактировать", + "challenge.admin.tasks.list.button.delete": "Удалить", + "challenge.admin.tasks.deleted": "Задание удалено", + "challenge.admin.tasks.delete.error": "Не удалось удалить задание", + "challenge.admin.tasks.list.loading": "Загрузка заданий...", + "challenge.admin.tasks.list.load.error": "Не удалось загрузить список заданий", + "challenge.admin.tasks.delete.confirm.title": "Удалить задание", + "challenge.admin.tasks.delete.confirm.message": "Вы уверены, что хотите удалить задание \"{title}\"? Это действие нельзя отменить.", + "challenge.admin.tasks.delete.confirm.button": "Удалить", + "challenge.admin.chains.updated": "Цепочка обновлена", + "challenge.admin.chains.created": "Цепочка создана", + "challenge.admin.chains.validation.enter.name": "Введите название цепочки", + "challenge.admin.chains.validation.add.task": "Добавьте хотя бы одно задание", + "challenge.admin.chains.save.error": "Не удалось сохранить цепочку", + "challenge.admin.chains.loading": "Загрузка цепочки...", + "challenge.admin.chains.load.error": "Не удалось загрузить цепочку", + "challenge.admin.chains.tasks.load.error": "Не удалось загрузить список заданий", + "challenge.admin.chains.edit.title": "Редактировать цепочку", + "challenge.admin.chains.create.title": "Создать цепочку", + "challenge.admin.chains.field.name": "Название цепочки", + "challenge.admin.chains.field.name.placeholder": "Введите название цепочки", + "challenge.admin.chains.selected.tasks": "Задания в цепочке", + "challenge.admin.chains.selected.tasks.empty": "Добавьте задания из списка ниже", + "challenge.admin.chains.available.tasks": "Доступные задания", + "challenge.admin.chains.search.placeholder": "Поиск заданий...", + "challenge.admin.chains.all.tasks.added": "Все задания уже добавлены", + "challenge.admin.chains.button.add": "+ Добавить", + "challenge.admin.chains.button.save": "Сохранить изменения", + "challenge.admin.chains.button.create": "Создать цепочку", + "challenge.admin.chains.list.title": "Цепочки заданий", + "challenge.admin.chains.list.create.button": "+ Создать цепочку", + "challenge.admin.chains.list.search.placeholder": "Поиск по названию...", + "challenge.admin.chains.list.empty.title": "Нет цепочек", + "challenge.admin.chains.list.empty.description": "Создайте первую цепочку заданий", + "challenge.admin.chains.list.empty.action": "Создать цепочку", + "challenge.admin.chains.list.search.empty": "По запросу \"{query}\" ничего не найдено", + "challenge.admin.chains.list.table.name": "Название", + "challenge.admin.chains.list.table.tasks.count": "Количество заданий", + "challenge.admin.chains.list.table.created": "Дата создания", + "challenge.admin.chains.list.table.actions": "Действия", + "challenge.admin.chains.list.badge.tasks": "заданий", + "challenge.admin.chains.list.button.edit": "Редактировать", + "challenge.admin.chains.list.button.delete": "Удалить", + "challenge.admin.chains.deleted": "Цепочка удалена", + "challenge.admin.chains.delete.error": "Не удалось удалить цепочку", + "challenge.admin.chains.list.loading": "Загрузка цепочек...", + "challenge.admin.chains.list.load.error": "Не удалось загрузить список цепочек", + "challenge.admin.chains.delete.confirm.title": "Удалить цепочку", + "challenge.admin.chains.delete.confirm.message": "Вы уверены, что хотите удалить цепочку \"{name}\"? Это действие нельзя отменить.", + "challenge.admin.chains.delete.confirm.button": "Удалить", + "challenge.admin.dashboard.title": "Dashboard", + "challenge.admin.dashboard.loading": "Загрузка статистики...", + "challenge.admin.dashboard.load.error": "Не удалось загрузить статистику системы", + "challenge.admin.dashboard.stats.users": "Всего пользователей", + "challenge.admin.dashboard.stats.tasks": "Всего заданий", + "challenge.admin.dashboard.stats.chains": "Всего цепочек", + "challenge.admin.dashboard.stats.submissions": "Всего проверок", + "challenge.admin.dashboard.submissions.title": "Статистика проверок", + "challenge.admin.dashboard.submissions.accepted": "Принято", + "challenge.admin.dashboard.submissions.rejected": "Отклонено", + "challenge.admin.dashboard.submissions.pending": "Ожидают", + "challenge.admin.dashboard.submissions.in.progress": "В процессе", + "challenge.admin.dashboard.queue.title": "Статус очереди", + "challenge.admin.dashboard.queue.processing": "В обработке", + "challenge.admin.dashboard.queue.waiting": "Ожидают в очереди", + "challenge.admin.dashboard.queue.total": "Всего в очереди", + "challenge.admin.dashboard.queue.utilization": "Загруженность очереди:", + "challenge.admin.dashboard.check.time.title": "Среднее время проверки", + "challenge.admin.dashboard.check.time.value": "{{time}} сек", + "challenge.admin.dashboard.check.time.description": "Время от отправки решения до получения результата", + "challenge.admin.users.title": "Пользователи", + "challenge.admin.users.loading": "Загрузка пользователей...", + "challenge.admin.users.load.error": "Не удалось загрузить список пользователей", + "challenge.admin.users.search.placeholder": "Поиск по nickname...", + "challenge.admin.users.empty.title": "Нет пользователей", + "challenge.admin.users.empty.description": "Пользователи появятся после регистрации", + "challenge.admin.users.search.empty": "По запросу \"{query}\" ничего не найдено", + "challenge.admin.users.table.nickname": "Nickname", + "challenge.admin.users.table.id": "ID", + "challenge.admin.users.table.registered": "Дата регистрации", + "challenge.admin.users.table.actions": "Действия", + "challenge.admin.users.button.stats": "Статистика", + "challenge.admin.users.stats.title": "Статистика пользователя", + "challenge.admin.users.stats.loading": "Загрузка статистики...", + "challenge.admin.users.stats.no.data": "Нет данных", + "challenge.admin.users.stats.completed": "Выполнено", + "challenge.admin.users.stats.total.submissions": "Всего попыток", + "challenge.admin.users.stats.in.progress": "В процессе", + "challenge.admin.users.stats.needs.revision": "Требует доработки", + "challenge.admin.users.stats.chains.progress": "Прогресс по цепочкам", + "challenge.admin.users.stats.tasks": "Задания", + "challenge.admin.users.stats.status.completed": "Завершено", + "challenge.admin.users.stats.status.needs_revision": "Доработка", + "challenge.admin.users.stats.status.in_progress": "В процессе", + "challenge.admin.users.stats.status.not_started": "Не начато", + "challenge.admin.users.stats.attempts": "Попыток:", + "challenge.admin.users.stats.avg.check.time": "Среднее время проверки", + "challenge.admin.users.stats.close": "Закрыть", + "challenge.admin.submissions.title": "Попытки решений", + "challenge.admin.submissions.loading": "Загрузка попыток...", + "challenge.admin.submissions.load.error": "Не удалось загрузить список попыток", + "challenge.admin.submissions.search.placeholder": "Поиск по пользователю или заданию...", + "challenge.admin.submissions.filter.status": "Статус", + "challenge.admin.submissions.status.all": "Все статусы", + "challenge.admin.submissions.status.accepted": "Принято", + "challenge.admin.submissions.status.needs_revision": "Доработка", + "challenge.admin.submissions.status.in_progress": "Проверяется", + "challenge.admin.submissions.status.pending": "Ожидает", + "challenge.admin.submissions.empty.title": "Нет попыток", + "challenge.admin.submissions.empty.description": "Попытки появятся после отправки решений", + "challenge.admin.submissions.search.empty.title": "Ничего не найдено", + "challenge.admin.submissions.search.empty.description": "Попробуйте изменить фильтры", + "challenge.admin.submissions.table.user": "Пользователь", + "challenge.admin.submissions.table.task": "Задание", + "challenge.admin.submissions.table.status": "Статус", + "challenge.admin.submissions.table.attempt": "Попытка", + "challenge.admin.submissions.table.submitted": "Дата отправки", + "challenge.admin.submissions.table.check.time": "Время проверки", + "challenge.admin.submissions.table.actions": "Действия", + "challenge.admin.submissions.button.details": "Детали", + "challenge.admin.submissions.check.time": "{{time}} сек", + "challenge.admin.submissions.details.title": "Детали попытки", + "challenge.admin.submissions.details.user": "Пользователь", + "challenge.admin.submissions.details.status": "Статус", + "challenge.admin.submissions.details.submitted": "Отправлено:", + "challenge.admin.submissions.details.checked": "Проверено:", + "challenge.admin.submissions.details.check.time": "Время проверки:", + "challenge.admin.submissions.details.task": "Задание:", + "challenge.admin.submissions.details.solution": "Решение пользователя:", + "challenge.admin.submissions.details.feedback": "Обратная связь от LLM:", + "challenge.admin.submissions.details.close": "Закрыть", + "challenge.admin.layout.title": "Challenge Admin", + "challenge.admin.layout.nav.dashboard": "Dashboard", + "challenge.admin.layout.nav.tasks": "Задания", + "challenge.admin.layout.nav.chains": "Цепочки", + "challenge.admin.layout.nav.users": "Пользователи", + "challenge.admin.layout.nav.submissions": "Попытки", + "challenge.admin.layout.button.player": "Открыть проигрыватель", + "challenge.admin.layout.button.logout": "Выйти", + "challenge.admin.common.loading.default": "Загрузка...", + "challenge.admin.common.error.default": "Произошла ошибка при загрузке данных", + "challenge.admin.common.retry": "Попробовать снова", + "challenge.admin.common.confirm": "Подтвердить", + "challenge.admin.common.close": "Закрыть" } \ No newline at end of file diff --git a/src/app.tsx b/src/app.tsx index 7480e5d..c6daa2c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom' import { Dashboard } from './dashboard' import { Provider } from './theme' import { Provider as ReduxProvider } from 'react-redux' -import { Toaster } from './components/ui/toaster' +import { ToasterProvider, Toaster } from './components/ui/toaster' import type { PropsWithChildren } from 'react' const App = ({ store }: PropsWithChildren<{ store?: any }>) => { @@ -14,10 +14,12 @@ const App = ({ store }: PropsWithChildren<{ store?: any }>) => { return ( - - - - + + + + + + ) diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index b6ae72d..e1310ba 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { DialogRoot, DialogContent, @@ -27,10 +28,14 @@ export const ConfirmDialog: React.FC = ({ onConfirm, title, message, - confirmLabel = 'Подтвердить', - cancelLabel = 'Отмена', + confirmLabel, + cancelLabel, isLoading = false, }) => { + const { t } = useTranslation() + + const confirm = confirmLabel || t('challenge.admin.common.confirm') + const cancel = cancelLabel || t('challenge.admin.common.cancel') return ( !e.open && onClose()}> @@ -43,15 +48,15 @@ export const ConfirmDialog: React.FC = ({ diff --git a/src/components/ErrorAlert.tsx b/src/components/ErrorAlert.tsx index c5f0d1f..e57fdf5 100644 --- a/src/components/ErrorAlert.tsx +++ b/src/components/ErrorAlert.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Box, Text, Button } from '@chakra-ui/react' interface ErrorAlertProps { @@ -7,9 +8,11 @@ interface ErrorAlertProps { } export const ErrorAlert: React.FC = ({ - message = 'Произошла ошибка при загрузке данных', + message, onRetry, }) => { + const { t } = useTranslation() + return ( = ({ textAlign="center" > - {message} + {message || t('challenge.admin.common.error.default')} {onRetry && ( )} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 4f29600..7645e14 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Link, useLocation, useNavigate } from 'react-router-dom' import { Box, Container, Flex, HStack, VStack, Button, Text } from '@chakra-ui/react' import { useAppSelector } from '../__data__/store' @@ -10,6 +11,7 @@ interface LayoutProps { } export const Layout: React.FC = ({ children }) => { + const { t, i18n } = useTranslation() const location = useLocation() const navigate = useNavigate() const user = useAppSelector((state) => state.user) @@ -22,16 +24,20 @@ export const Layout: React.FC = ({ children }) => { navigate(URLs.challengePlayer) } + const handleChangeLanguage = (lang: string) => { + i18n.changeLanguage(lang) + } + const isActive = (path: string) => { return location.pathname === path } const navItems = [ - { label: 'Dashboard', path: URLs.dashboard }, - { label: 'Задания', path: URLs.tasks }, - { label: 'Цепочки', path: URLs.chains }, - { label: 'Пользователи', path: URLs.users }, - { label: 'Попытки', path: URLs.submissions }, + { label: t('challenge.admin.layout.nav.dashboard'), path: URLs.dashboard }, + { label: t('challenge.admin.layout.nav.tasks'), path: URLs.tasks }, + { label: t('challenge.admin.layout.nav.chains'), path: URLs.chains }, + { label: t('challenge.admin.layout.nav.users'), path: URLs.users }, + { label: t('challenge.admin.layout.nav.submissions'), path: URLs.submissions }, ] return ( @@ -41,16 +47,38 @@ export const Layout: React.FC = ({ children }) => { - Challenge Admin + {t('challenge.admin.layout.title')} + {/* Language Switcher */} + + + + + {user && ( @@ -64,7 +92,7 @@ export const Layout: React.FC = ({ children }) => { variant="ghost" onClick={handleLogout} > - Выйти + {t('challenge.admin.layout.button.logout')} )} diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx index 4aebbbb..d8f5dd1 100644 --- a/src/components/LoadingSpinner.tsx +++ b/src/components/LoadingSpinner.tsx @@ -1,16 +1,19 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Flex, Spinner, Text, VStack } from '@chakra-ui/react' interface LoadingSpinnerProps { message?: string } -export const LoadingSpinner: React.FC = ({ message = 'Загрузка...' }) => { +export const LoadingSpinner: React.FC = ({ message }) => { + const { t } = useTranslation() + return ( - {message} + {message || t('challenge.admin.common.loading.default')} ) diff --git a/src/components/StatusBadge.tsx b/src/components/StatusBadge.tsx index eb476ca..d0597bc 100644 --- a/src/components/StatusBadge.tsx +++ b/src/components/StatusBadge.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Badge } from '@chakra-ui/react' import type { SubmissionStatus } from '../types/challenge' @@ -7,6 +8,8 @@ interface StatusBadgeProps { } export const StatusBadge: React.FC = ({ status }) => { + const { t } = useTranslation() + const getColorPalette = () => { switch (status) { case 'accepted': @@ -22,24 +25,9 @@ export const StatusBadge: React.FC = ({ status }) => { } } - const getLabel = () => { - switch (status) { - case 'accepted': - return 'Принято' - case 'needs_revision': - return 'Доработка' - case 'in_progress': - return 'Проверяется' - case 'pending': - return 'Ожидает' - default: - return status - } - } - return ( - {getLabel()} + {t(`challenge.admin.submissions.status.${status}`)} ) } diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx index 31ab7cd..b992ce8 100644 --- a/src/components/ui/toaster.tsx +++ b/src/components/ui/toaster.tsx @@ -1,12 +1,198 @@ -import React from 'react' -import { createToaster, Toaster as ChakraToaster } from '@chakra-ui/react' +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react' +import { createPortal } from 'react-dom' +import { Box, Text, HStack, IconButton } from '@chakra-ui/react' -export const toaster = createToaster({ - placement: 'top-end', - duration: 3000, -}) +type ToastType = 'success' | 'error' | 'info' | 'warning' -export const Toaster = () => { - return +interface Toast { + id: string + title: string + description?: string + type: ToastType + duration?: number } +interface ToasterContextValue { + create: (toast: Omit) => void +} + +const ToasterContext = createContext(null) + +export const useToaster = () => { + const context = useContext(ToasterContext) + if (!context) { + throw new Error('useToaster must be used within ToasterProvider') + } + return context +} + +const getToastColor = (type: ToastType) => { + switch (type) { + case 'success': + return { bg: 'green.500', color: 'white' } + case 'error': + return { bg: 'red.500', color: 'white' } + case 'warning': + return { bg: 'orange.500', color: 'white' } + case 'info': + return { bg: 'blue.500', color: 'white' } + } +} + +const getToastIcon = (type: ToastType) => { + switch (type) { + case 'success': + return '✓' + case 'error': + return '✕' + case 'warning': + return '⚠' + case 'info': + return 'ℹ' + } +} + +export const ToasterProvider = ({ children }: { children: ReactNode }) => { + const [toasts, setToasts] = useState([]) + + const create = useCallback((toast: Omit) => { + const id = Math.random().toString(36).substring(2, 9) + const newToast = { ...toast, id } + + setToasts((prev) => [...prev, newToast]) + + const duration = toast.duration ?? 3000 + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, duration) + }, []) + + const remove = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, []) + + return ( + + {children} + {typeof window !== 'undefined' && + createPortal( + + {toasts.map((toast) => { + const colors = getToastColor(toast.type) + return ( + + + + + {getToastIcon(toast.type)} + + + + {toast.title} + + {toast.description && ( + + {toast.description} + + )} + + + remove(toast.id)} + _hover={{ bg: 'whiteAlpha.300' }} + > + ✕ + + + + ) + })} + , + document.body + )} + + ) +} + +// Singleton instance для совместимости со старым API +class ToasterSingleton { + private static instance: ToasterSingleton + private createFn: ((toast: Omit) => void) | null = null + + static getInstance(): ToasterSingleton { + if (!ToasterSingleton.instance) { + ToasterSingleton.instance = new ToasterSingleton() + } + return ToasterSingleton.instance + } + + setCreateFn(fn: (toast: Omit) => void) { + this.createFn = fn + } + + create(toast: Omit) { + if (this.createFn) { + this.createFn(toast) + } else { + console.error('Toaster not initialized') + } + } +} + +export const toaster = ToasterSingleton.getInstance() + +// Компонент для инициализации singleton +export const ToasterInitializer = () => { + const { create } = useToaster() + + React.useEffect(() => { + toaster.setCreateFn(create) + }, [create]) + + return null +} + +// Старый компонент Toaster для совместимости +export const Toaster = () => { + return ( + <> + + + + ) +} diff --git a/src/pages/chains/ChainFormPage.tsx b/src/pages/chains/ChainFormPage.tsx index e7cc4ba..e487569 100644 --- a/src/pages/chains/ChainFormPage.tsx +++ b/src/pages/chains/ChainFormPage.tsx @@ -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, @@ -29,6 +30,7 @@ export const ChainFormPage: React.FC = () => { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const isEdit = !!id + const { t } = useTranslation() const { data: chain, isLoading: isLoadingChain, error: loadError } = useGetChainQuery(id!, { skip: !id, @@ -53,8 +55,8 @@ export const ChainFormPage: React.FC = () => { if (!name.trim()) { toaster.create({ - title: 'Ошибка валидации', - description: 'Введите название цепочки', + title: t('challenge.admin.common.validation.error'), + description: t('challenge.admin.chains.validation.enter.name'), type: 'error', }) return @@ -62,8 +64,8 @@ export const ChainFormPage: React.FC = () => { if (selectedTasks.length === 0) { toaster.create({ - title: 'Ошибка валидации', - description: 'Добавьте хотя бы одно задание', + title: t('challenge.admin.common.validation.error'), + description: t('challenge.admin.chains.validation.add.task'), type: 'error', }) return @@ -81,8 +83,8 @@ export const ChainFormPage: React.FC = () => { }, }).unwrap() toaster.create({ - title: 'Успешно', - description: 'Цепочка обновлена', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.updated'), type: 'success', }) } else { @@ -91,16 +93,24 @@ export const ChainFormPage: React.FC = () => { tasks: taskIds, }).unwrap() toaster.create({ - title: 'Успешно', - description: 'Цепочка создана', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.created'), type: 'success', }) } navigate(URLs.chains) - } 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.chains.save.error') + toaster.create({ - title: 'Ошибка', - description: err?.data?.error?.message || 'Не удалось сохранить цепочку', + title: t('challenge.admin.common.error'), + description: errorMessage, type: 'error', }) } @@ -131,19 +141,19 @@ export const ChainFormPage: React.FC = () => { } if (isEdit && isLoadingChain) { - return + return } if (isEdit && loadError) { - return + return } if (isLoadingTasks) { - return + return } if (!allTasks) { - return + return } const isLoading = isCreating || isUpdating @@ -156,7 +166,7 @@ export const ChainFormPage: React.FC = () => { return ( - {isEdit ? 'Редактировать цепочку' : 'Создать цепочку'} + {isEdit ? t('challenge.admin.chains.edit.title') : t('challenge.admin.chains.create.title')} { {/* Name */} - Название цепочки + {t('challenge.admin.chains.field.name')} setName(e.target.value)} - placeholder="Введите название цепочки" + placeholder={t('challenge.admin.chains.field.name.placeholder')} maxLength={255} disabled={isLoading} /> @@ -184,7 +194,7 @@ export const ChainFormPage: React.FC = () => { {/* Selected Tasks */} - Задания в цепочке ({selectedTasks.length}) + {t('challenge.admin.chains.selected.tasks')} ({selectedTasks.length}) {selectedTasks.length === 0 ? ( { borderRadius="md" textAlign="center" > - Добавьте задания из списка ниже + {t('challenge.admin.chains.selected.tasks.empty')} ) : ( @@ -255,10 +265,10 @@ export const ChainFormPage: React.FC = () => { {/* Available Tasks */} - Доступные задания + {t('challenge.admin.chains.available.tasks')} setSearchQuery(e.target.value)} mb={3} @@ -273,8 +283,8 @@ export const ChainFormPage: React.FC = () => { > {allTasks.length === selectedTasks.length - ? 'Все задания уже добавлены' - : 'Ничего не найдено'} + ? t('challenge.admin.chains.all.tasks.added') + : t('challenge.admin.common.not.found')} ) : ( @@ -295,7 +305,7 @@ export const ChainFormPage: React.FC = () => { > {task.title} ))} @@ -306,10 +316,10 @@ export const ChainFormPage: React.FC = () => { {/* Actions */} - diff --git a/src/pages/chains/ChainsListPage.tsx b/src/pages/chains/ChainsListPage.tsx index f8ee337..fd6ee41 100644 --- a/src/pages/chains/ChainsListPage.tsx +++ b/src/pages/chains/ChainsListPage.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { Box, Heading, @@ -22,6 +23,7 @@ import { toaster } from '../../components/ui/toaster' export const ChainsListPage: React.FC = () => { const navigate = useNavigate() + const { t } = useTranslation() const { data: chains, isLoading, error, refetch } = useGetChainsQuery() const [deleteChain, { isLoading: isDeleting }] = useDeleteChainMutation() @@ -34,26 +36,26 @@ export const ChainsListPage: React.FC = () => { try { await deleteChain(chainToDelete.id).unwrap() toaster.create({ - title: 'Успешно', - description: 'Цепочка удалена', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.deleted'), type: 'success', }) setChainToDelete(null) } catch (err) { toaster.create({ - title: 'Ошибка', - description: 'Не удалось удалить цепочку', + title: t('challenge.admin.common.error'), + description: t('challenge.admin.chains.delete.error'), type: 'error', }) } } if (isLoading) { - return + return } if (error || !chains) { - return + return } const filteredChains = chains.filter((chain) => @@ -71,16 +73,16 @@ export const ChainsListPage: React.FC = () => { return ( - Цепочки заданий + {t('challenge.admin.chains.list.title')} {chains.length > 0 && ( setSearchQuery(e.target.value)} maxW="400px" @@ -90,25 +92,25 @@ export const ChainsListPage: React.FC = () => { {filteredChains.length === 0 && chains.length === 0 ? ( navigate(URLs.chainNew)} /> ) : filteredChains.length === 0 ? ( ) : ( - Название - Количество заданий - Дата создания - Действия + {t('challenge.admin.chains.list.table.name')} + {t('challenge.admin.chains.list.table.tasks.count')} + {t('challenge.admin.chains.list.table.created')} + {t('challenge.admin.chains.list.table.actions')} @@ -117,7 +119,7 @@ export const ChainsListPage: React.FC = () => { {chain.name} - {chain.tasks.length} заданий + {chain.tasks.length} {t('challenge.admin.chains.list.badge.tasks')} @@ -132,7 +134,7 @@ export const ChainsListPage: React.FC = () => { variant="ghost" onClick={() => navigate(URLs.chainEdit(chain.id))} > - Редактировать + {t('challenge.admin.chains.list.button.edit')} @@ -155,9 +157,9 @@ export const ChainsListPage: React.FC = () => { isOpen={!!chainToDelete} onClose={() => setChainToDelete(null)} onConfirm={handleDeleteChain} - title="Удалить цепочку" - message={`Вы уверены, что хотите удалить цепочку "${chainToDelete?.name}"? Это действие нельзя отменить.`} - confirmLabel="Удалить" + title={t('challenge.admin.chains.delete.confirm.title')} + message={t('challenge.admin.chains.delete.confirm.message', { name: chainToDelete?.name })} + confirmLabel={t('challenge.admin.chains.delete.confirm.button')} isLoading={isDeleting} /> diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 0c3711c..649178b 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Box, Heading, Grid, Text, VStack, HStack, Badge, Progress } from '@chakra-ui/react' import { useGetSystemStatsQuery } from '../../__data__/api/api' import { StatCard } from '../../components/StatCard' @@ -6,16 +7,17 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { ErrorAlert } from '../../components/ErrorAlert' export const DashboardPage: React.FC = () => { + const { t } = useTranslation() const { data: stats, isLoading, error, refetch } = useGetSystemStatsQuery(undefined, { pollingInterval: 10000, // Обновление каждые 10 секунд }) if (isLoading) { - return + return } if (error || !stats) { - return + return } const acceptanceRate = stats.submissions.total > 0 @@ -32,25 +34,25 @@ export const DashboardPage: React.FC = () => { return ( - Dashboard + {t('challenge.admin.dashboard.title')} {/* Main Stats */} - - - - + + + + {/* Submissions Stats */} - Статистика проверок + {t('challenge.admin.dashboard.submissions.title')} - Принято + {t('challenge.admin.dashboard.submissions.accepted')} @@ -62,7 +64,7 @@ export const DashboardPage: React.FC = () => { - Отклонено + {t('challenge.admin.dashboard.submissions.rejected')} @@ -74,7 +76,7 @@ export const DashboardPage: React.FC = () => { - Ожидают + {t('challenge.admin.dashboard.submissions.pending')} {stats.submissions.pending} @@ -83,7 +85,7 @@ export const DashboardPage: React.FC = () => { - В процессе + {t('challenge.admin.dashboard.submissions.in.progress')} {stats.submissions.inProgress} @@ -95,13 +97,13 @@ export const DashboardPage: React.FC = () => { {/* Queue Stats */} - Статус очереди + {t('challenge.admin.dashboard.queue.title')} - В обработке + {t('challenge.admin.dashboard.queue.processing')} @@ -115,7 +117,7 @@ export const DashboardPage: React.FC = () => { - Ожидают в очереди + {t('challenge.admin.dashboard.queue.waiting')} {stats.queue.waiting} @@ -124,7 +126,7 @@ export const DashboardPage: React.FC = () => { - Всего в очереди + {t('challenge.admin.dashboard.queue.total')} {stats.queue.queueLength} @@ -134,7 +136,7 @@ export const DashboardPage: React.FC = () => { - Загруженность очереди: {queueUtilization}% + {t('challenge.admin.dashboard.queue.utilization')} {queueUtilization}% @@ -147,13 +149,13 @@ export const DashboardPage: React.FC = () => { {/* Average Check Time */} - Среднее время проверки + {t('challenge.admin.dashboard.check.time.title')} - {(stats.averageCheckTimeMs / 1000).toFixed(2)} сек + {t('challenge.admin.dashboard.check.time.value', { time: (stats.averageCheckTimeMs / 1000).toFixed(2) })} - Время от отправки решения до получения результата + {t('challenge.admin.dashboard.check.time.description')} diff --git a/src/pages/index.ts b/src/pages/index.ts deleted file mode 100644 index 22fff66..0000000 --- a/src/pages/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { lazy } from 'react' - -export const MainPage = lazy(() => import(/* webpackChunkName: 'main' */'./main')) - diff --git a/src/pages/main/index.ts b/src/pages/main/index.ts deleted file mode 100644 index 52a5afb..0000000 --- a/src/pages/main/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { MainPage as default } from './main' - diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx deleted file mode 100644 index d9bb34e..0000000 --- a/src/pages/main/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' - -export const MainPage = () => { - return ( -
-

Главная страница проекта challenge-admin-pl

-

Это базовая страница с React Router

-
- ) -} - diff --git a/src/pages/submissions/SubmissionsPage.tsx b/src/pages/submissions/SubmissionsPage.tsx index e7d6ffe..d26910e 100644 --- a/src/pages/submissions/SubmissionsPage.tsx +++ b/src/pages/submissions/SubmissionsPage.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Box, Heading, @@ -27,6 +28,7 @@ import { StatusBadge } from '../../components/StatusBadge' import type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser } from '../../types/challenge' export const SubmissionsPage: React.FC = () => { + const { t } = useTranslation() const { data: submissions, isLoading, error, refetch } = useGetAllSubmissionsQuery() const [searchQuery, setSearchQuery] = useState('') @@ -34,11 +36,11 @@ export const SubmissionsPage: React.FC = () => { const [selectedSubmission, setSelectedSubmission] = useState(null) if (isLoading) { - return + return } if (error || !submissions) { - return + return } const filteredSubmissions = submissions.filter((submission) => { @@ -69,28 +71,28 @@ export const SubmissionsPage: React.FC = () => { const submitted = new Date(submission.submittedAt).getTime() const checked = new Date(submission.checkedAt).getTime() const diff = Math.round((checked - submitted) / 1000) - return `${diff} сек` + return t('challenge.admin.submissions.check.time', { time: diff }) } const statusOptions = createListCollection({ items: [ - { label: 'Все статусы', value: 'all' }, - { label: 'Принято', value: 'accepted' }, - { label: 'Доработка', value: 'needs_revision' }, - { label: 'Проверяется', value: 'in_progress' }, - { label: 'Ожидает', value: 'pending' }, + { label: t('challenge.admin.submissions.status.all'), value: 'all' }, + { label: t('challenge.admin.submissions.status.accepted'), value: 'accepted' }, + { label: t('challenge.admin.submissions.status.needs.revision'), value: 'needs_revision' }, + { label: t('challenge.admin.submissions.status.in.progress'), value: 'in_progress' }, + { label: t('challenge.admin.submissions.status.pending'), value: 'pending' }, ], }) return ( - Попытки решений + {t('challenge.admin.submissions.title')} {/* Filters */} {submissions.length > 0 && ( setSearchQuery(e.target.value)} maxW="400px" @@ -102,7 +104,7 @@ export const SubmissionsPage: React.FC = () => { maxW="200px" > - + {statusOptions.items.map((option) => ( @@ -116,21 +118,21 @@ export const SubmissionsPage: React.FC = () => { )} {filteredSubmissions.length === 0 && submissions.length === 0 ? ( - + ) : filteredSubmissions.length === 0 ? ( - + ) : ( - Пользователь - Задание - Статус - Попытка - Дата отправки - Время проверки - Действия + {t('challenge.admin.submissions.table.user')} + {t('challenge.admin.submissions.table.task')} + {t('challenge.admin.submissions.table.status')} + {t('challenge.admin.submissions.table.attempt')} + {t('challenge.admin.submissions.table.submitted')} + {t('challenge.admin.submissions.table.check.time')} + {t('challenge.admin.submissions.table.actions')} @@ -167,7 +169,7 @@ export const SubmissionsPage: React.FC = () => { colorPalette="teal" onClick={() => setSelectedSubmission(submission)} > - Детали + {t('challenge.admin.submissions.button.details')} @@ -199,6 +201,8 @@ const SubmissionDetailsModal: React.FC = ({ isOpen, onClose, }) => { + const { t } = useTranslation() + if (!submission) return null const user = submission.user as ChallengeUser @@ -215,7 +219,7 @@ const SubmissionDetailsModal: React.FC = ({ }) } - const getCheckTime = () => { + const getCheckTimeValue = () => { if (!submission.checkedAt) return null const submitted = new Date(submission.submittedAt).getTime() const checked = new Date(submission.checkedAt).getTime() @@ -226,7 +230,7 @@ const SubmissionDetailsModal: React.FC = ({ !e.open && onClose()} size="xl"> - Детали попытки #{submission.attemptNumber} + {t('challenge.admin.submissions.details.title')} #{submission.attemptNumber} @@ -235,13 +239,13 @@ const SubmissionDetailsModal: React.FC = ({ - Пользователь + {t('challenge.admin.submissions.details.user')} {user.nickname} - Статус + {t('challenge.admin.submissions.details.status')} @@ -249,15 +253,15 @@ const SubmissionDetailsModal: React.FC = ({ - Отправлено: {formatDate(submission.submittedAt)} + {t('challenge.admin.submissions.details.submitted')} {formatDate(submission.submittedAt)} {submission.checkedAt && ( <> - Проверено: {formatDate(submission.checkedAt)} + {t('challenge.admin.submissions.details.checked')} {formatDate(submission.checkedAt)} - Время проверки: {getCheckTime()} сек + {t('challenge.admin.submissions.details.check.time')} {t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })} )} @@ -267,7 +271,7 @@ const SubmissionDetailsModal: React.FC = ({ {/* Task */} - Задание: {task.title} + {t('challenge.admin.submissions.details.task')} {task.title} = ({ {/* Solution */} - Решение пользователя: + {t('challenge.admin.submissions.details.solution')} = ({ {submission.feedback && ( - Обратная связь от LLM: + {t('challenge.admin.submissions.details.feedback')} = ({ diff --git a/src/pages/tasks/TaskFormPage.tsx b/src/pages/tasks/TaskFormPage.tsx index c8c3b7e..7884239 100644 --- a/src/pages/tasks/TaskFormPage.tsx +++ b/src/pages/tasks/TaskFormPage.tsx @@ -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 + return } if (isEdit && loadError) { - return + return } const isLoading = isCreating || isUpdating return ( - {isEdit ? 'Редактировать задание' : 'Создать задание'} + {isEdit ? t('challenge.admin.tasks.edit.title') : t('challenge.admin.tasks.create.title')} { {/* Title */} - Название задания + {t('challenge.admin.tasks.field.title')} setTitle(e.target.value)} - placeholder="Введите название задания" + placeholder={t('challenge.admin.tasks.field.title.placeholder')} maxLength={255} disabled={isLoading} /> - Максимум 255 символов + {t('challenge.admin.tasks.field.title.helper')} {/* Description with Markdown */} - Описание (Markdown) + {t('challenge.admin.tasks.field.description')} setShowDescPreview(e.value === 'preview')} > - Редактор - Превью + {t('challenge.admin.tasks.tab.editor')} + {t('challenge.admin.tasks.tab.preview')}