feat: add multilingualism (#35) #39
| @ -13,5 +13,41 @@ | ||||
|   "dry-wash.landing.hero-section.headline": "Revitalize Your Ride with Eco-Friendly Care!", | ||||
|   "dry-wash.landing.make-order-button": "Make order", | ||||
|   "dry-wash.landing.site-logo": "The logo of the \"Dry Master\" company", | ||||
|   "dry-wash.landing.social-proof-section.heading": "We are being chosen" | ||||
|   "dry-wash.landing.social-proof-section.heading": "We are being chosen", | ||||
|   "dry-wash.arm.master.add": "Add", | ||||
|   "dry-wash.arm.order.title": "Orders", | ||||
|   "dry-wash.arm.order.status.progress": "In Progress", | ||||
|   "dry-wash.arm.order.status.complete": "Completed", | ||||
|   "dry-wash.arm.order.status.pending": "Pending", | ||||
|   "dry-wash.arm.order.status.working": "Working", | ||||
|   "dry-wash.arm.order.status.canceled": "Canceled", | ||||
|   "dry-wash.arm.order.status.placeholder": "Select Status", | ||||
|   "dry-wash.arm.order.table.header.carNumber": "Car Number", | ||||
|   "dry-wash.arm.order.table.header.washingTime": "Washing Time", | ||||
|   "dry-wash.arm.order.table.header.orderDate": "Order Date", | ||||
|   "dry-wash.arm.order.table.header.status": "Status", | ||||
|   "dry-wash.arm.order.table.header.telephone": "Telephone", | ||||
|   "dry-wash.arm.order.table.header.location": "Location", | ||||
|   "dry-wash.arm.master.title": "Masters", | ||||
|   "dry-wash.arm.master.table.header.name": "Name", | ||||
|   "dry-wash.arm.master.table.header.currentJob": "Current Job", | ||||
|   "dry-wash.arm.master.table.header.phone": "Phone", | ||||
|   "dry-wash.arm.master.table.header.actions": "Actions", | ||||
|   "dry-wash.arm.master.table.actionsMenu.delete": "Delete Master", | ||||
|   "dry-wash.arm.master.drawer.title": "Add New Master", | ||||
|   "dry-wash.arm.master.drawer.inputName.label": "Full Name", | ||||
|   "dry-wash.arm.master.drawer.inputName.placeholder": "Enter Full Name", | ||||
|   "dry-wash.arm.master.drawer.inputPhone.label": "Phone Number", | ||||
|   "dry-wash.arm.master.drawer.inputPhone.placeholder": "Enter Phone Number", | ||||
|   "dry-wash.arm.master.drawer.button.save": "Save", | ||||
|   "dry-wash.arm.master.drawer.button.cancel": "Cancel", | ||||
|   "dry-wash.arm.master.sideBar.orders": "Orders", | ||||
|   "dry-wash.arm.master.sideBar.master": "Masters", | ||||
|   "dry-wash.arm.master.sideBar.title": "Dry Master", | ||||
|   "dry-wash.notFound.title": "Page Not Found", | ||||
|   "dry-wash.notFound.description": "Unfortunately, the page you are looking for does not exist.", | ||||
|   "dry-wash.notFound.button.back": "Back to Home", | ||||
|   "dry-wash.errorBoundary.title": "Something went wrong", | ||||
|   "dry-wash.errorBoundary.description": "We are already working on fixing the issue", | ||||
|   "dry-wash.errorBoundary.button.reload": "Reload Page" | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
|   "dry-wash.arm.order.title": "Заказы", | ||||
|   "dry-wash.arm.order.status.progress": "Выполняется", | ||||
|   "dry-wash.arm.order.status.complete": "Завершено", | ||||
|   "dry-wash.arm.order.status.pending": "в ожидании", | ||||
|   "dry-wash.arm.order.status.pending": "В ожидании", | ||||
|   "dry-wash.arm.order.status.working": "В работе", | ||||
|   "dry-wash.arm.order.status.canceled": "Отменено", | ||||
|   "dry-wash.arm.order.status.placeholder": "Выберите статус", | ||||
| @ -26,9 +26,9 @@ | ||||
|   "dry-wash.arm.master.drawer.inputPhone.placeholder": "Введите номер телефона", | ||||
|   "dry-wash.arm.master.drawer.button.save": "Сохранить", | ||||
|   "dry-wash.arm.master.drawer.button.cancel": "Отменить", | ||||
|   "dry-wash.arm.master.sideBar.title": " Сухой мастер", | ||||
|   "dry-wash.arm.master.sideBar.title.master": "Мастера", | ||||
|   "dry-wash.arm.master.sideBar.title.orders": "Заказы", | ||||
|   "dry-wash.arm.master.sideBar.orders": "Заказы", | ||||
|   "dry-wash.arm.master.sideBar.master": "Мастера", | ||||
|   "dry-wash.arm.master.sideBar.title": "Сухой мастер", | ||||
|   "dry-wash.landing.benefits-section.description": "Откройте для себя преимущества наших услуг по химчистке автомобилей", | ||||
|   "dry-wash.landing.benefits-section.heading": "Преимущества экологичной автомойки", | ||||
|   "dry-wash.landing.benefits-section.list.0": "Экологически безопасные продукты", | ||||
| @ -46,8 +46,8 @@ | ||||
|   "dry-wash.landing.social-proof-section.heading": "Нас выбирают", | ||||
|   "dry-wash.notFound.title": "Страница не найдена", | ||||
|   "dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.", | ||||
|   "dry-wash.notFound.button.back": " Вернуться на главную", | ||||
|   "dry-wash.notFound.button.back": "Вернуться на главную", | ||||
|   "dry-wash.errorBoundary.title":"Что-то пошло не так", | ||||
|   "dry-wash.errorBoundary.description": " Мы уже работаем над исправлением проблемы", | ||||
|   "dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы", | ||||
|   "dry-wash.errorBoundary.button.reload": "Перезагрузить страницу" | ||||
| } | ||||
|  | ||||
| @ -34,15 +34,16 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { | ||||
|   render() { | ||||
|     const { hasError } = this.state; | ||||
|     //TODO: добавить анимацию после залива 404 страницы
 | ||||
|     //TODO: может сделать обертку для хука, чтоб язык менялся без перезагрузки
 | ||||
|     if (hasError) { | ||||
|       return ( | ||||
|         <Center minH='100vh'> | ||||
|           <VStack spacing={4} textAlign='center'> | ||||
|             <Heading as='h1' size='2xl'> | ||||
|               {i18next.t('dry-wash.errorBoundary.title')} | ||||
|               {i18next.t('~:dry-wash.errorBoundary.title')} | ||||
|             </Heading> | ||||
|             <Text fontSize='lg'> | ||||
|               {i18next.t('dry-wash.errorBoundary.description')} | ||||
|               {i18next.t('~:dry-wash.errorBoundary.description')} | ||||
|             </Text> | ||||
|             <Button | ||||
|               colorScheme='teal' | ||||
| @ -50,7 +51,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { | ||||
|               variant='outline' | ||||
|               onClick={() => window.location.reload()} | ||||
|             > | ||||
|               {i18next.t('dry-wash.errorBoundary.button.reload')} | ||||
|               {i18next.t('~:dry-wash.errorBoundary.button.reload')} | ||||
|             </Button> | ||||
|           </VStack> | ||||
|         </Center> | ||||
|  | ||||
| @ -7,16 +7,18 @@ import { | ||||
|   IconButton, | ||||
| } from '@chakra-ui/react'; | ||||
| import { EditIcon } from '@chakra-ui/icons'; | ||||
| import i18next from 'i18next'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 
 | ||||
| const MasterActionsMenu = () => { | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.arm.master.table.actionsMenu', | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Menu> | ||||
|       <MenuButton icon={<EditIcon />} as={IconButton} variant='outline' /> | ||||
|       <MenuList> | ||||
|         <MenuItem> | ||||
|           {i18next.t('dry-wash.arm.master.table.actionsMenu.delete')} | ||||
|         </MenuItem> | ||||
|         <MenuItem>{t('delete')}</MenuItem> | ||||
|       </MenuList> | ||||
|     </Menu> | ||||
|   ); | ||||
|  | ||||
| @ -12,7 +12,7 @@ import { | ||||
|   DrawerHeader, | ||||
|   DrawerOverlay, | ||||
| } from '@chakra-ui/react'; | ||||
| import i18next from 'i18next'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 
 | ||||
| const MasterDrawer = ({ isOpen, onClose }) => { | ||||
|   const [newMaster, setNewMaster] = useState({ name: '', phone: '' }); | ||||
| @ -22,51 +22,44 @@ const MasterDrawer = ({ isOpen, onClose }) => { | ||||
|     onClose(); | ||||
|   }; | ||||
| 
 | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.arm.master.drawer', | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Drawer isOpen={isOpen} onClose={onClose} size='md'> | ||||
|       <DrawerOverlay /> | ||||
|       <DrawerContent> | ||||
|         <DrawerCloseButton /> | ||||
|         <DrawerHeader> | ||||
|           {i18next.t('dry-wash.arm.master.drawer.title')} | ||||
|         </DrawerHeader> | ||||
|         <DrawerHeader>{t('title')}</DrawerHeader> | ||||
|         <DrawerBody> | ||||
|           <FormControl mb='4'> | ||||
|             <FormLabel> | ||||
|               {i18next.t('dry-wash.arm.master.drawer.inputName.label')} | ||||
|             </FormLabel> | ||||
|             <FormLabel>{t('inputName.label')}</FormLabel> | ||||
|             <Input | ||||
|               value={newMaster.name} | ||||
|               onChange={(e) => | ||||
|                 setNewMaster({ ...newMaster, name: e.target.value }) | ||||
|               } | ||||
|               placeholder={i18next.t( | ||||
|                 'dry-wash.arm.master.drawer.inputName.placeholder', | ||||
|               )} | ||||
|               placeholder={t('inputName.placeholder')} | ||||
|             /> | ||||
|           </FormControl> | ||||
|           <FormControl> | ||||
|             <FormLabel> | ||||
|               {' '} | ||||
|               {i18next.t('dry-wash.arm.master.drawer.inputPhone.label')} | ||||
|             </FormLabel> | ||||
|             <FormLabel> {t('inputPhone.label')}</FormLabel> | ||||
|             <Input | ||||
|               value={newMaster.phone} | ||||
|               onChange={(e) => | ||||
|                 setNewMaster({ ...newMaster, phone: e.target.value }) | ||||
|               } | ||||
|               placeholder={i18next.t( | ||||
|                 'dry-wash.arm.master.drawer.inputPhone.placeholder', | ||||
|               )} | ||||
|               placeholder={t('inputPhone.placeholder')} | ||||
|             /> | ||||
|           </FormControl> | ||||
|         </DrawerBody> | ||||
|         <DrawerFooter> | ||||
|           <Button colorScheme='teal' mr={3} onClick={handleSave}> | ||||
|             {i18next.t('dry-wash.arm.master.drawer.button.save')} | ||||
|             {t('button.save')} | ||||
|           </Button> | ||||
|           <Button variant='ghost' onClick={onClose}> | ||||
|             {i18next.t('dry-wash.arm.master.drawer.button.cancel')} | ||||
|             {t('button.cancel')} | ||||
|           </Button> | ||||
|         </DrawerFooter> | ||||
|       </DrawerContent> | ||||
|  | ||||
| @ -11,32 +11,39 @@ import { | ||||
|   useDisclosure, | ||||
|   Flex, | ||||
| } from '@chakra-ui/react'; | ||||
| import i18next from 'i18next'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 
 | ||||
| import { mastersData } from '../../mocks'; | ||||
| import MasterItem from '../MasterItem'; | ||||
| import MasterDrawer from '../MasterDrawer'; | ||||
| 
 | ||||
| const TABLE_HEADERS = ['name', 'currentJob', 'phone', 'actions']; | ||||
| const TABLE_HEADERS = [ | ||||
|   'name' as const, | ||||
|   'currentJob' as const, | ||||
|   'phone' as const, | ||||
|   'actions' as const, | ||||
| ]; | ||||
| 
 | ||||
| const Masters = () => { | ||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||
| 
 | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.arm.master', | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Box p='8'> | ||||
|       <Flex justifyContent='space-between' alignItems='center' mb='5'> | ||||
|         <Heading size='lg'> {i18next.t('dry-wash.arm.master.title')}</Heading> | ||||
|         <Heading size='lg'> {t('title')}</Heading> | ||||
|         <Button colorScheme='green' onClick={onOpen}> | ||||
|           + {i18next.t('dry-wash.arm.master.add')} | ||||
|           + {t('add')} | ||||
|         </Button> | ||||
|       </Flex> | ||||
|       <Table variant='simple' colorScheme='blackAlpha'> | ||||
|         <Thead> | ||||
|           <Tr> | ||||
|             {TABLE_HEADERS.map((name) => ( | ||||
|               <Th key={name}> | ||||
|                 {i18next.t(`dry-wash.arm.master.table.header.${name}`)} | ||||
|               </Th> | ||||
|               <Th key={name}>{t(`table.header.${name}`)}</Th> | ||||
|             ))} | ||||
|           </Tr> | ||||
|         </Thead> | ||||
|  | ||||
| @ -1,8 +1,26 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import { Td, Tr, Link, Select } from '@chakra-ui/react'; | ||||
| import i18next from 'i18next'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 
 | ||||
| const statuses = ['pending', 'progress', 'working', 'canceled', 'complete']; | ||||
| const statuses = [ | ||||
|   'pending' as const, | ||||
|   'progress' as const, | ||||
|   'working' as const, | ||||
|   'canceled' as const, | ||||
|   'complete' as const, | ||||
| ]; | ||||
| 
 | ||||
| type GetArrItemType<ArrType> = | ||||
|   ArrType extends Array<infer ItemType> ? ItemType : never; | ||||
| 
 | ||||
| export type OrderProps = { | ||||
|   carNumber?: string; | ||||
|   washTime?: string; | ||||
|   orderDate?: string; | ||||
|   status?: GetArrItemType<typeof statuses>; | ||||
|   phone?: string; | ||||
|   location?: string; | ||||
| }; | ||||
| 
 | ||||
| const OrderItem = ({ | ||||
|   carNumber, | ||||
| @ -11,7 +29,11 @@ const OrderItem = ({ | ||||
|   status, | ||||
|   phone, | ||||
|   location, | ||||
| }) => { | ||||
| }: OrderProps) => { | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.arm.order', | ||||
|   }); | ||||
| 
 | ||||
|   const [statusSelect, setStatus] = useState(status); | ||||
| 
 | ||||
|   return ( | ||||
| @ -22,12 +44,12 @@ const OrderItem = ({ | ||||
|       <Td> | ||||
|         <Select | ||||
|           value={statusSelect} | ||||
|           onChange={(e) => setStatus(e.target.value)} | ||||
|           placeholder={i18next.t(`dry-wash.arm.order.status.placeholder`)} | ||||
|           onChange={(e) => setStatus(e.target.value as OrderProps['status'])} | ||||
|           placeholder={t(`status.placeholder`)} | ||||
|         > | ||||
|           {statuses.map((status) => ( | ||||
|             <option key={status} value={status}> | ||||
|               {i18next.t(`dry-wash.arm.order.status.${status}`)} | ||||
|               {t(`status.${status}`)} | ||||
|             </option> | ||||
|           ))} | ||||
|         </Select> | ||||
|  | ||||
| @ -1,36 +1,45 @@ | ||||
| import { Box, Heading, Table, Thead, Tbody, Tr, Th } from '@chakra-ui/react'; | ||||
| import React from 'react'; | ||||
| import i18next from 'i18next'; | ||||
| import { Box, Heading, Table, Thead, Tbody, Tr, Th } from '@chakra-ui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 
 | ||||
| import { ordersData } from '../../mocks'; | ||||
| import OrderItem from '../OrderItem'; | ||||
| import { OrderProps } from '../OrderItem/OrderItem'; | ||||
| 
 | ||||
| const Orders = () => { | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.arm.order', | ||||
|   }); | ||||
| 
 | ||||
|   const TABLE_HEADERS = [ | ||||
|     'carNumber', | ||||
|     'washingTime', | ||||
|     'orderDate', | ||||
|     'status', | ||||
|     'telephone', | ||||
|     'location', | ||||
|     'carNumber' as const, | ||||
|     'washingTime' as const, | ||||
|     'orderDate' as const, | ||||
|     'status' as const, | ||||
|     'telephone' as const, | ||||
|     'location' as const, | ||||
|   ]; | ||||
| 
 | ||||
|   return ( | ||||
|     <Box p='8'> | ||||
|       <Heading size='lg' mb='5'> | ||||
|         {i18next.t('dry-wash.arm.order.title')} | ||||
|         {t('title')} | ||||
|       </Heading> | ||||
|       <Table variant='simple' colorScheme='blackAlpha'> | ||||
|         <Thead> | ||||
|           <Tr> | ||||
|             {TABLE_HEADERS.map((name, key) => ( | ||||
|               <Th key={key}> | ||||
|                 {i18next.t(`dry-wash.arm.order.table.header.${name}`)} | ||||
|               </Th> | ||||
|               <Th key={key}>{t(`table.header.${name}`)}</Th> | ||||
|             ))} | ||||
|           </Tr> | ||||
|         </Thead> | ||||
|         <Tbody> | ||||
|           {ordersData.map((order, index) => ( | ||||
|             <OrderItem key={index} {...order} /> | ||||
|             <OrderItem | ||||
|               key={index} | ||||
|               {...order} | ||||
|               status={order.status as OrderProps['status']} | ||||
|             /> | ||||
|           ))} | ||||
|         </Tbody> | ||||
|       </Table> | ||||
|  | ||||
| @ -1,46 +1,51 @@ | ||||
| import { Box, Button, Heading, VStack } from '@chakra-ui/react'; | ||||
| import { Box, Button, Heading, VStack, Divider } from '@chakra-ui/react'; | ||||
| import React from 'react'; | ||||
| import { Divider } from '@chakra-ui/react'; | ||||
| import i18next from 'i18next'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 
 | ||||
| const Sidebar = () => ( | ||||
|   <Box | ||||
|     borderRight='1px solid black' | ||||
|     bg='gray.50' | ||||
|     color='white' | ||||
|     w='250px' | ||||
|     p='5' | ||||
|     pt='8' | ||||
|   > | ||||
|     <Heading color='green' size='lg' mb='5'> | ||||
|       {i18next.t(`dry-wash.arm.master.sideBar.title`)} | ||||
|     </Heading> | ||||
| const Sidebar = () => { | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.arm.master.sideBar', | ||||
|   }); | ||||
| 
 | ||||
|     <VStack align='start' spacing='4'> | ||||
|       <Divider /> | ||||
|       <Button | ||||
|         as={Link} | ||||
|         to='orders' | ||||
|         w='100%' | ||||
|         colorScheme='green' | ||||
|         variant='ghost' | ||||
|       > | ||||
|         {i18next.t(`dry-wash.arm.master.sideBar.title.orders`)} | ||||
|       </Button> | ||||
|       <Divider /> | ||||
|       <Button | ||||
|         as={Link} | ||||
|         to='masters' | ||||
|         w='100%' | ||||
|         colorScheme='green' | ||||
|         variant='ghost' | ||||
|       > | ||||
|         {i18next.t(`dry-wash.arm.master.sideBar.title.master`)} | ||||
|       </Button> | ||||
|       <Divider /> | ||||
|     </VStack> | ||||
|   </Box> | ||||
| ); | ||||
|   return ( | ||||
|     <Box | ||||
|       borderRight='1px solid black' | ||||
|       bg='gray.50' | ||||
|       color='white' | ||||
|       w='250px' | ||||
|       p='5' | ||||
|       pt='8' | ||||
|     > | ||||
|       <Heading color='green' size='lg' mb='5'> | ||||
|         {t('title')} | ||||
|       </Heading> | ||||
| 
 | ||||
|       <VStack align='start' spacing='4'> | ||||
|         <Divider /> | ||||
|         <Button | ||||
|           as={Link} | ||||
|           to='orders' | ||||
|           w='100%' | ||||
|           colorScheme='green' | ||||
|           variant='ghost' | ||||
|         > | ||||
|           {t('orders')} | ||||
|         </Button> | ||||
|         <Divider /> | ||||
|         <Button | ||||
|           as={Link} | ||||
|           to='masters' | ||||
|           w='100%' | ||||
|           colorScheme='green' | ||||
|           variant='ghost' | ||||
|         > | ||||
|           {t('master')} | ||||
|         </Button> | ||||
|         <Divider /> | ||||
|       </VStack> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Sidebar; | ||||
|  | ||||
| @ -15,7 +15,7 @@ export const CtaButton: FC<ButtonProps> = (props) => { | ||||
|       colorScheme='primary' | ||||
|       {...props} | ||||
|     > | ||||
|       {t('dry-wash.landing.make-order-button')} | ||||
|       {t('~:dry-wash.landing.make-order-button')} | ||||
|     </Button> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -7,7 +7,7 @@ import { LogoSvg } from '../../../assets/icons'; | ||||
| export const SiteLogo: FC = () => { | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|   return <Image src={LogoSvg} alt={t('dry-wash.landing.site-logo')} w={40} />; | ||||
|   return <Image src={LogoSvg} alt={t('~:dry-wash.landing.site-logo')} w={40} />; | ||||
| }; | ||||
| 
 | ||||
| // todo: replace Image by SVG React component
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import LayoutArm from '../../components/LayoutArm'; | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user