diff --git a/Dockerfile b/Dockerfile index fd12b82..44805b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # Build commands: -# - $VERSION=0.6 +# - $VERSION=0.7 # - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build # - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "." # - docker image push --all-tags oncharterliz/multi-frigate diff --git a/package.json b/package.json index 7d67329..fb984ea 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "date-fns": "^3.3.1", "dayjs": "^1.11.9", "embla-carousel-react": "^8.0.0-rc10", + "i18next": "^23.10.1", + "i18next-browser-languagedetector": "^7.2.0", "idb-keyval": "^6.2.1", "jwt-decode": "^4.0.0", "mantine-react-table": "^1.0.0-beta.25", @@ -39,6 +41,7 @@ "react": "^18.2.0", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-i18next": "^14.1.0", "react-oidc-context": "^2.2.2", "react-router-dom": "^6.14.1", "react-scripts": "5.0.1", diff --git a/src/App.tsx b/src/App.tsx index 15a8de1..df52dc4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,6 +62,7 @@ function App() { urlParams.delete('state'); urlParams.delete('session_state'); urlParams.delete('code'); + urlParams.delete('iss'); navigate(`${location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`, { replace: true }) } diff --git a/src/AppBody.tsx b/src/AppBody.tsx index 3da0dc4..d558902 100644 --- a/src/AppBody.tsx +++ b/src/AppBody.tsx @@ -1,14 +1,24 @@ -import React, { useContext, useState } from 'react'; -import { AppShell, useMantineTheme, } from "@mantine/core" -import { HeaderAction } from './widgets/header/HeaderAction'; -import { headerLinks } from './widgets/header/header.links'; -import AppRouter from './router/AppRouter'; -import { Context } from '.'; -import SideBar from './shared/components/SideBar'; +import { AppShell, useMantineTheme, } from "@mantine/core"; import { observer } from 'mobx-react-lite'; +import { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Context } from '.'; +import AppRouter from './router/AppRouter'; +import { routesPath } from './router/routes.path'; +import SideBar from './shared/components/SideBar'; import { isProduction } from './shared/env.const'; +import { HeaderAction } from './widgets/header/HeaderAction'; const AppBody = () => { + const { t } = useTranslation() + + const headerLinks = [ + { link: routesPath.MAIN_PATH, label: t('header.home') }, + { link: routesPath.SETTINGS_PATH, label: t('header.settings'), admin: true }, + { link: routesPath.RECORDINGS_PATH, label: t('header.recordings') }, + { link: routesPath.HOSTS_PATH, label: t('header.hostsConfig'), admin: true }, + { link: routesPath.ACCESS_PATH, label: t('header.acessSettings'), admin: true }, + ] const { sideBarsStore } = useContext(Context) diff --git a/src/index.tsx b/src/index.tsx index b3f0426..6eb45c2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,6 +6,7 @@ import RootStore from './shared/stores/root.store'; import { AuthProvider, AuthProviderProps } from 'react-oidc-context'; import { isProduction, oidpSettings } from './shared/env.const'; import { BrowserRouter } from 'react-router-dom'; +import './services/i18n'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement @@ -23,6 +24,7 @@ export const keycloakConfig: AuthProviderProps = { params.delete('state'); params.delete('session_state'); params.delete('code'); + params.delete('iss'); const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}` window.history.replaceState({}, document.title, newUrl) } diff --git a/src/locales/en.ts b/src/locales/en.ts new file mode 100644 index 0000000..ce5b35f --- /dev/null +++ b/src/locales/en.ts @@ -0,0 +1,63 @@ +const en = { + header: { + home: 'Main', + settings: 'Settings', + recordings: 'Recordings', + hostsConfig: 'Frigate servers', + acessSettings: 'Access settings', + }, hostArr: { + host: 'Host', + name: 'Host name', + url: 'Address', + enabled: 'Enabled', + }, + player: { + startVideo: 'Enable Video', + stopVideo: 'Disable Video', + object: 'Object', + duration: 'Duration', + startTime: 'Start', + endTime: 'End', + doubleClickToFullHint: 'Double click for fullscreen', + rating: 'Rating', + }, + pleaseSelectRole: 'Please select Role', + pleaseSelectHost: 'Please select Host', + pleaseSelectCamera: 'Please select Camera', + pleaseSelectDate: 'Please select Date', + nothingHere: 'Nothing here', + allowed: 'Allowed', + notAllowed: 'Not allowed', + camera: 'Camera', + camersDoesNotExist: 'No cameras', + search: 'Search', + recordings: 'Recordings', + hour: 'Hour', + events: 'Events', + notHaveEvents: 'No events', + day: 'Day', + selectHost: 'Select host', + selectCamera: 'Select Camera', + selectRange: 'Select period', + changeTheme: "Change theme", + logout: "Logout", + enterQuantity: "Enter quantity:", + quantity: "Quantity", + tooltipСlose: "Press Enter", + hide: "Hide", + confirm: "Confirm", + save: "Save", + discard: "Cancel", + next: "Next", + back: "Back", + goToMainPage: "Return to main page", + retry: "Retry", + youCanRetryOrGoToMain: "You can retry or return to the main page", + errors: { + somthingGoesWrong: "Something went wrong", + 403: "Sorry, you do not have access", + 404: "Sorry, we cannot find that page", + } +} + +export default en \ No newline at end of file diff --git a/src/locales/ru.ts b/src/locales/ru.ts new file mode 100644 index 0000000..25e31ab --- /dev/null +++ b/src/locales/ru.ts @@ -0,0 +1,64 @@ +const ru = { + header: { + home: 'Главная', + settings: 'Настройки', + recordings: 'Записи', + hostsConfig: 'Серверы Frigate', + acessSettings: 'Настройка доступа', + }, + hostArr: { + host: 'Хост', + name: 'Имя хоста', + url: 'Адрес', + enabled: 'Включен', + }, + player: { + startVideo: 'Вкл. Видео', + stopVideo: 'Выкл. Видео', + object: 'Объект', + duration: 'Длительность', + startTime: 'Начало', + endTime: 'Конец', + doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра', + rating: 'Рейтинг', + }, + pleaseSelectRole: 'Пожалуйста выберите роль', + pleaseSelectHost: 'Пожалуйста выберите хост', + pleaseSelectCamera: 'Пожалуйста выберите камеру', + pleaseSelectDate: 'Пожалуйста выберите дату', + nothingHere: 'Ничего нет', + allowed: 'Разрешено', + notAllowed: 'Не разрешено', + camera: 'Камера', + camersDoesNotExist: 'Камер нет', + search: 'Поиск', + recordings: 'Записи', + hour: 'Час', + events: 'События', + notHaveEvents: 'Событий нет', + day: 'День', + selectHost:'Выбери хост', + selectCamera: 'Выбери камеру', + selectRange: 'Выбери период', + changeTheme: "Изменить тему", + logout: "Выйти", + enterQuantity: "Введите количество:", + quantity: "Количество", + tooltipСlose: "Нажмите Enter", + hide: "Скрыть", + confirm: "Подтвердить", + save: "Сохранить", + discard: "Отменить", + next: "Далее", + back: "Назад", + goToMainPage: "Вернуться на главную", + retry: "Повторить", + youCanRetryOrGoToMain: "Вы можете повторить или вернуться на главную", + errors: { + somthingGoesWrong: "Что-то пошло не так", + 403: "Извините, у вас нет доступа", + 404: "Извините, мы не можем найти такую страницу", + } +} + +export default ru \ No newline at end of file diff --git a/src/pages/403.tsx b/src/pages/403.tsx index 2397137..2952199 100644 --- a/src/pages/403.tsx +++ b/src/pages/403.tsx @@ -1,12 +1,13 @@ import { Button, Flex, Text } from '@mantine/core'; -import React, { useContext, useEffect, useRef } from 'react'; -import CogWheelWithText from '../shared/components/loaders/CogWheelWithText'; -import { strings } from '../shared/strings/strings'; -import { routesPath } from '../router/routes.path'; -import { Context } from '..'; import { observer } from 'mobx-react-lite'; +import { useContext, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Context } from '..'; +import { routesPath } from '../router/routes.path'; +import CogWheelWithText from '../shared/components/loaders/CogWheelWithText'; const Forbidden = () => { + const { t } = useTranslation() const executed = useRef(false) const { sideBarsStore } = useContext(Context) @@ -25,9 +26,9 @@ const Forbidden = () => { return ( - {strings.errors[403]} + {t('errors.403')} - + ); diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 94e0f8e..6a35057 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,12 +1,13 @@ import { Button, Flex, Text } from '@mantine/core'; import React, { useContext, useEffect, useRef } from 'react'; import CogWheelWithText from '../shared/components/loaders/CogWheelWithText'; -import { strings } from '../shared/strings/strings'; import { routesPath } from '../router/routes.path'; import { Context } from '..'; import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; const NotFound = () => { + const { t } = useTranslation() const executed = useRef(false) const { sideBarsStore } = useContext(Context) @@ -25,9 +26,9 @@ const NotFound = () => { return ( - {strings.errors[404]} + {t('errors.404')} - + ); diff --git a/src/pages/AccessSettingsPage.tsx b/src/pages/AccessSettingsPage.tsx index 37a9029..18eb94c 100644 --- a/src/pages/AccessSettingsPage.tsx +++ b/src/pages/AccessSettingsPage.tsx @@ -1,21 +1,22 @@ -import { useQuery } from '@tanstack/react-query'; -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; -import CenterLoader from '../shared/components/loaders/CenterLoader'; -import RetryErrorPage from './RetryErrorPage'; import { Flex, Group, Select, Text } from '@mantine/core'; -import { OneSelectItem } from '../shared/components/filters/OneSelectFilter'; import { useMediaQuery } from '@mantine/hooks'; -import { dimensions } from '../shared/dimensions/dimensions'; -import CamerasTransferList from '../shared/components/CamerasTransferList'; -import { Context } from '..'; -import { strings } from '../shared/strings/strings'; -import { useAdminRole } from '../hooks/useAdminRole'; -import Forbidden from './403'; +import { useQuery } from '@tanstack/react-query'; import { observer } from 'mobx-react-lite'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Context } from '..'; +import { useAdminRole } from '../hooks/useAdminRole'; +import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; +import CamerasTransferList from '../shared/components/CamerasTransferList'; +import { OneSelectItem } from '../shared/components/filters/OneSelectFilter'; +import CenterLoader from '../shared/components/loaders/CenterLoader'; +import { dimensions } from '../shared/dimensions/dimensions'; import { isProduction } from '../shared/env.const'; +import Forbidden from './403'; +import RetryErrorPage from './RetryErrorPage'; const AccessSettings = () => { + const { t } = useTranslation() const executed = useRef(false) const { data, isPending, isError, refetch } = useQuery({ queryKey: [frigateQueryKeys.getRoles], @@ -49,7 +50,7 @@ const AccessSettings = () => { if (!isProduction) console.log('AccessSettings rendered') return ( - {strings.pleaseSelectRole} + {t('pleaseSelectRole')} {!isMobile ? : <>}