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

View File

@@ -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 <LoadingSpinner message="Загрузка цепочек..." />
return <LoadingSpinner message={t('challenge.admin.chains.list.loading')} />
}
if (error || !chains) {
return <ErrorAlert message="Не удалось загрузить список цепочек" onRetry={refetch} />
return <ErrorAlert message={t('challenge.admin.chains.list.load.error')} onRetry={refetch} />
}
const filteredChains = chains.filter((chain) =>
@@ -71,16 +73,16 @@ export const ChainsListPage: React.FC = () => {
return (
<Box>
<Flex justify="space-between" align="center" mb={6}>
<Heading>Цепочки заданий</Heading>
<Heading>{t('challenge.admin.chains.list.title')}</Heading>
<Button colorPalette="teal" onClick={() => navigate(URLs.chainNew)}>
+ Создать цепочку
{t('challenge.admin.chains.list.create.button')}
</Button>
</Flex>
{chains.length > 0 && (
<Box mb={4}>
<Input
placeholder="Поиск по названию..."
placeholder={t('challenge.admin.chains.list.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
maxW="400px"
@@ -90,25 +92,25 @@ export const ChainsListPage: React.FC = () => {
{filteredChains.length === 0 && chains.length === 0 ? (
<EmptyState
title="Нет цепочек"
description="Создайте первую цепочку заданий"
actionLabel="Создать цепочку"
title={t('challenge.admin.chains.list.empty.title')}
description={t('challenge.admin.chains.list.empty.description')}
actionLabel={t('challenge.admin.chains.list.empty.action')}
onAction={() => navigate(URLs.chainNew)}
/>
) : filteredChains.length === 0 ? (
<EmptyState
title="Ничего не найдено"
description={`По запросу "${searchQuery}" ничего не найдено`}
title={t('challenge.admin.common.not.found')}
description={t('challenge.admin.chains.list.search.empty', { query: searchQuery })}
/>
) : (
<Box bg="white" borderRadius="lg" boxShadow="sm" borderWidth="1px" borderColor="gray.200" overflowX="auto">
<Table.Root size="sm">
<Table.Header>
<Table.Row>
<Table.ColumnHeader>Название</Table.ColumnHeader>
<Table.ColumnHeader>Количество заданий</Table.ColumnHeader>
<Table.ColumnHeader>Дата создания</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">Действия</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.name')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.tasks.count')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.created')}</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">{t('challenge.admin.chains.list.table.actions')}</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -117,7 +119,7 @@ export const ChainsListPage: React.FC = () => {
<Table.Cell fontWeight="medium">{chain.name}</Table.Cell>
<Table.Cell>
<Badge colorPalette="teal" variant="subtle">
{chain.tasks.length} заданий
{chain.tasks.length} {t('challenge.admin.chains.list.badge.tasks')}
</Badge>
</Table.Cell>
<Table.Cell>
@@ -132,7 +134,7 @@ export const ChainsListPage: React.FC = () => {
variant="ghost"
onClick={() => navigate(URLs.chainEdit(chain.id))}
>
Редактировать
{t('challenge.admin.chains.list.button.edit')}
</Button>
<Button
size="sm"
@@ -140,7 +142,7 @@ export const ChainsListPage: React.FC = () => {
colorPalette="red"
onClick={() => setChainToDelete(chain)}
>
Удалить
{t('challenge.admin.chains.list.button.delete')}
</Button>
</HStack>
</Table.Cell>
@@ -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}
/>
</Box>