diff --git a/build-all.sh b/build-all.sh index 4ea903f..e977862 100755 --- a/build-all.sh +++ b/build-all.sh @@ -34,6 +34,7 @@ OUTPUT="${ROOT}/dist/TorrServer" #### Build web echo "Build web" cd "${ROOT}/web" || exit 1 +npm install --silent npm run --silent build-js cp "${ROOT}/web/dest/index.html" "${ROOT}/server/web/pages/template/pages/" diff --git a/web/package.json b/web/package.json index 67224e3..51d7989 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,8 @@ "axios": "^0.21.1", "clsx": "^1.1.1", "fontsource-roboto": "^4.0.0", + "i18next": "^20.3.1", + "i18next-browser-languagedetector": "^6.1.1", "konva": "^8.0.1", "lodash": "^4.17.21", "material-ui-image": "^3.3.2", @@ -16,6 +18,7 @@ "react-copy-to-clipboard": "^5.0.3", "react-div-100vh": "^0.6.0", "react-dom": "^17.0.2", + "react-i18next": "^11.10.0", "react-konva": "^17.0.2-4", "react-measure": "^2.5.2", "react-scripts": "4.0.3", diff --git a/web/public/index.html b/web/public/index.html index 1fe47d0..41b4208 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -2,7 +2,7 @@ - + diff --git a/web/src/App/Sidebar.jsx b/web/src/App/Sidebar.jsx index 0f911c8..b6cf730 100644 --- a/web/src/App/Sidebar.jsx +++ b/web/src/App/Sidebar.jsx @@ -11,21 +11,23 @@ import UploadDialog from 'components/Upload' import { CreditCard as CreditCardIcon, List as ListIcon } from '@material-ui/icons' import List from '@material-ui/core/List' import CloseServer from 'components/CloseServer' +import { useTranslation } from 'react-i18next' import { AppSidebarStyle } from './style' export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { + const { t } = useTranslation() return ( - + - + @@ -44,7 +46,7 @@ export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { - + diff --git a/web/src/App/index.jsx b/web/src/App/index.jsx index ce7164a..3cfe795 100644 --- a/web/src/App/index.jsx +++ b/web/src/App/index.jsx @@ -16,7 +16,7 @@ import Sidebar from './Sidebar' const baseTheme = createMuiTheme({ overrides: { MuiCssBaseline: { '@global': { html: { WebkitFontSmoothing: 'auto' } } } }, - palette: { primary: { main: '#3fb57a' }, secondary: { main: '#FFA724' }, tonalOffset: 0.2 }, + palette: { primary: { main: '#00a572' }, secondary: { main: '#ffa724' }, tonalOffset: 0.2 }, }) export default function App() { diff --git a/web/src/App/style.js b/web/src/App/style.js index bebad80..436b758 100644 --- a/web/src/App/style.js +++ b/web/src/App/style.js @@ -2,6 +2,7 @@ import styled, { css } from 'styled-components' export const AppWrapper = styled.div` height: 100%; + background: #cbe8d9; display: grid; grid-template-columns: 60px 1fr; grid-template-rows: 60px 1fr; @@ -17,7 +18,7 @@ export const CenteredGrid = styled.div` ` export const AppHeader = styled.div` - background: #3fb57a; + background: #00a572; color: rgba(0, 0, 0, 0.87); grid-area: head; display: flex; @@ -34,7 +35,7 @@ export const AppSidebarStyle = styled.div` overflow-x: hidden; transition: width 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms; border-right: 1px solid rgba(0, 0, 0, 0.12); - background: #fff; + background: #eee; white-space: nowrap; `} ` diff --git a/web/src/components/About.jsx b/web/src/components/About.jsx index a241eee..58cae8a 100644 --- a/web/src/components/About.jsx +++ b/web/src/components/About.jsx @@ -1,4 +1,5 @@ -import { useState } from 'react' +import axios from 'axios' +import { useEffect, useState } from 'react' import Button from '@material-ui/core/Button' import Dialog from '@material-ui/core/Dialog' import DialogActions from '@material-ui/core/DialogActions' @@ -8,9 +9,16 @@ import InfoIcon from '@material-ui/icons/Info' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' +import { useTranslation } from 'react-i18next' +import { echoHost } from 'utils/Hosts' export default function AboutDialog() { + const { t } = useTranslation() const [open, setOpen] = useState(false) + const [torrServerVersion, setTorrServerVersion] = useState('') + useEffect(() => { + axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data)) + }, []) return (
@@ -18,26 +26,30 @@ export default function AboutDialog() { - + setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'> - About + {t('About')} +
+

TorrServer {torrServerVersion}

+ https://github.com/YouROK/TorrServer +
-

Thanks to everyone who tested and helped.

+

{t('ThanksToEveryone')}


-

Special thanks:

- Anacrolix Matt Joiner github.com/anacrolix +

{t('SpecialThanks')}

+ anacrolix Matt Joiner github.com/anacrolix
- tsynik nikk Никита github.com/tsynik + nikk github.com/tsynik
dancheskus github.com/dancheskus
- Tw1cker Руслан Пахнев github.com/Nemiroff + tw1cker Руслан Пахнев github.com/Nemiroff
SpAwN_LMG
@@ -46,7 +58,7 @@ export default function AboutDialog() {
diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index 6d042fb..a1dd0c2 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -7,8 +7,10 @@ import DialogContent from '@material-ui/core/DialogContent' import DialogTitle from '@material-ui/core/DialogTitle' import { torrentsHost } from 'utils/Hosts' import axios from 'axios' +import { useTranslation } from 'react-i18next' export default function AddDialog({ handleClose }) { + const { t } = useTranslation() const [link, setLink] = useState('') const [title, setTitle] = useState('') const [poster, setPoster] = useState('') @@ -23,17 +25,17 @@ export default function AddDialog({ handleClose }) { return ( - Add magnet or link to torrent file + {t('AddMagnetOrLink')} - - + + @@ -41,11 +43,11 @@ export default function AddDialog({ handleClose }) { diff --git a/web/src/components/Add/index.jsx b/web/src/components/Add/index.jsx index b798785..8b449d9 100644 --- a/web/src/components/Add/index.jsx +++ b/web/src/components/Add/index.jsx @@ -3,12 +3,13 @@ import ListItemIcon from '@material-ui/core/ListItemIcon' import LibraryAddIcon from '@material-ui/icons/LibraryAdd' import ListItemText from '@material-ui/core/ListItemText' import ListItem from '@material-ui/core/ListItem' +import { useTranslation } from 'react-i18next' import AddDialog from './AddDialog' export default function AddDialogButton() { + const { t } = useTranslation() const [isDialogOpen, setIsDialogOpen] = useState(false) - const handleClickOpen = () => setIsDialogOpen(true) const handleClose = () => setIsDialogOpen(false) @@ -18,7 +19,7 @@ export default function AddDialogButton() { - + {isDialogOpen && } diff --git a/web/src/components/CloseServer.jsx b/web/src/components/CloseServer.jsx index 22d155b..90da371 100644 --- a/web/src/components/CloseServer.jsx +++ b/web/src/components/CloseServer.jsx @@ -2,27 +2,29 @@ import { useState } from 'react' import { Button, Dialog, DialogActions, DialogTitle, ListItem, ListItemIcon, ListItemText } from '@material-ui/core' import { PowerSettingsNew as PowerSettingsNewIcon } from '@material-ui/icons' import { shutdownHost } from 'utils/Hosts' +import { useTranslation } from 'react-i18next' export default function CloseServer() { + const { t } = useTranslation() const [open, setOpen] = useState(false) const closeDialog = () => setOpen(false) const openDialog = () => setOpen(true) return ( <> - + - + - Close server? + {t('CloseServer?')} diff --git a/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx b/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx index 520ed60..9b44400 100644 --- a/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx +++ b/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx @@ -2,6 +2,7 @@ import Button from '@material-ui/core/Button' import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' import { ArrowBack } from '@material-ui/icons' +import { useTranslation } from 'react-i18next' const useStyles = makeStyles(theme => ({ appBar: { position: 'relative' }, @@ -9,6 +10,7 @@ const useStyles = makeStyles(theme => ({ })) export default function DialogHeader({ title, onClose, onBack }) { + const { t } = useTranslation() const classes = useStyles() return ( @@ -24,7 +26,7 @@ export default function DialogHeader({ title, onClose, onBack }) { {onBack && ( )} diff --git a/web/src/components/DialogTorrentDetailsContent/Table/index.jsx b/web/src/components/DialogTorrentDetailsContent/Table/index.jsx index 11e56b7..1ae46b4 100644 --- a/web/src/components/DialogTorrentDetailsContent/Table/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/Table/index.jsx @@ -4,6 +4,7 @@ import { humanizeSize } from 'utils/Utils' import ptt from 'parse-torrent-title' import { Button } from '@material-ui/core' import CopyToClipboard from 'react-copy-to-clipboard' +import { useTranslation } from 'react-i18next' import { TableStyle, ShortTableWrapper, ShortTable } from './style' @@ -11,6 +12,7 @@ const { memo } = require('react') const Table = memo( ({ playableFileList, viewedFileList, selectedSeason, seasonAmount, hash }) => { + const { t } = useTranslation() const preloadBuffer = fileId => fetch(`${streamHost()}?link=${hash}&index=${fileId}&preload`) const getFileLink = (path, id) => `${streamHost()}/${encodeURIComponent(path.split('\\').pop().split('/').pop())}?link=${hash}&index=${id}&play` @@ -25,13 +27,13 @@ const Table = memo( - viewed - name - {fileHasSeasonText && seasonAmount?.length === 1 && season} - {fileHasEpisodeText && episode} - {fileHasResolutionText && resolution} - size - actions + {t('Viewed')} + {t('Name')} + {fileHasSeasonText && seasonAmount?.length === 1 && {t('Season')}} + {fileHasEpisodeText && {t('Episode')}} + {fileHasResolutionText && {t('Resolution')}} + {t('Size')} + {t('Actions')} @@ -52,18 +54,18 @@ const Table = memo( {humanizeSize(length)} @@ -87,7 +89,7 @@ const Table = memo(
{isViewed && (
-
viewed
+
{t('Viewed')}
@@ -95,41 +97,41 @@ const Table = memo( )} {fileHasSeasonText && seasonAmount?.length === 1 && (
-
season
+
{t('Season')}
{season}
)} {fileHasEpisodeText && (
-
epoisode
+
{t('Episode')}
{episode}
)} {fileHasResolutionText && (
-
resolution
+
{t('Resolution')}
{resolution}
)}
-
size
+
{t('Size')}
{humanizeSize(length)}
diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js index 50e3d3b..235be1d 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js @@ -1,7 +1,7 @@ export const defaultBorderColor = '#eef2f4' export const defaultBackgroundColor = '#fff' -export const completeColor = '#3fb57a' -export const progressColor = '#00d0d0' +export const completeColor = '#00a572' +export const progressColor = '#ffa724' export const activeColor = '#000' export const rangeColor = '#9a9aff' diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx index 3126544..ba2f9c4 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx @@ -4,12 +4,14 @@ import { playlistTorrHost, torrentsHost, viewedHost } from 'utils/Hosts' import { CopyToClipboard } from 'react-copy-to-clipboard' import { Button } from '@material-ui/core' import ptt from 'parse-torrent-title' +import { useTranslation } from 'react-i18next' import { SmallLabel, MainSectionButtonGroup } from './style' import { SectionSubName } from '../style' const TorrentFunctions = memo( ({ hash, viewedFileList, playableFileList, name, title, setViewedFileList }) => { + const { t } = useTranslation() const latestViewedFileId = viewedFileList?.[viewedFileList?.length - 1] const latestViewedFile = playableFileList?.find(({ id }) => id === latestViewedFileId)?.path const isOnlyOnePlayableFile = playableFileList?.length === 1 @@ -24,13 +26,13 @@ const TorrentFunctions = memo( <> {!isOnlyOnePlayableFile && !!viewedFileList?.length && ( <> - Download Playlist + {t('DownloadPlaylist')} - Latest file played: {latestViewedFileData?.title}. + {t('LatestFilePlayed')} {latestViewedFileData?.title}. {latestViewedFileData?.season && ( <> {' '} - Season: {latestViewedFileData?.season}. Episode: {latestViewedFileData?.episode}. + {t('Season')}: {latestViewedFileData?.season}. {t('Episode')}: {latestViewedFileData?.episode}. )} @@ -38,39 +40,39 @@ const TorrentFunctions = memo( )} - Torrent State + {t('TorrentState')} - Info + {t('Info')} {(isOnlyOnePlayableFile || !viewedFileList?.length) && ( )} diff --git a/web/src/components/DialogTorrentDetailsContent/index.jsx b/web/src/components/DialogTorrentDetailsContent/index.jsx index 200ddfc..e1cdd42 100644 --- a/web/src/components/DialogTorrentDetailsContent/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/index.jsx @@ -7,6 +7,7 @@ import axios from 'axios' import { viewedHost } from 'utils/Hosts' import { GETTING_INFO, IN_DB } from 'torrentStates' import CircularProgress from '@material-ui/core/CircularProgress' +import { useTranslation } from 'react-i18next' import { useUpdateCache, useGetSettings } from './customHooks' import DialogHeader from './DialogHeader' @@ -37,6 +38,7 @@ const Loader = () => ( ) export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { + const { t } = useTranslation() const [isLoading, setIsLoading] = useState(true) const [isDetailedCacheView, setIsDetailedCacheView] = useState(false) const [viewedFileList, setViewedFileList] = useState() @@ -99,6 +101,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { }, [hash]) const bufferSize = settings?.PreloadBuffer ? Capacity : 33554432 // Default is 32mb if PreloadBuffer is false + // const bufferSize = Capacity const getTitle = value => { const torrentParsedName = value && ptt.parse(value) @@ -115,7 +118,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { <> setIsDetailedCacheView(false) })} /> @@ -171,10 +174,8 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { - Buffer - {!settings?.PreloadBuffer && ( - Enable "Preload Buffer" in settings to change buffer size - )} + {t('Buffer')} + {!settings?.PreloadBuffer && {t('BufferNote')}} setIsDetailedCacheView(true)} > - Detailed cache view + {t('DetailedCacheView')} - Torrent Content + {t('TorrentContent')} {seasonAmount?.length > 1 && ( <> - Select Season + {t('SelectSeason')} {seasonAmount.map(season => ( - + diff --git a/web/src/components/RemoveAll.jsx b/web/src/components/RemoveAll.jsx index aac5c0f..be9c719 100644 --- a/web/src/components/RemoveAll.jsx +++ b/web/src/components/RemoveAll.jsx @@ -5,6 +5,7 @@ import ListItemText from '@material-ui/core/ListItemText' import DeleteIcon from '@material-ui/icons/Delete' import { useState } from 'react' import { torrentsHost } from 'utils/Hosts' +import { useTranslation } from 'react-i18next' const fnRemoveAll = () => { fetch(torrentsHost(), { @@ -31,25 +32,25 @@ const fnRemoveAll = () => { } export default function RemoveAll() { + const { t } = useTranslation() const [open, setOpen] = useState(false) const closeDialog = () => setOpen(false) const openDialog = () => setOpen(true) - return ( <> - + - + - Delete Torrent? + {t('DeleteTorrents?')} diff --git a/web/src/components/Settings.jsx b/web/src/components/Settings.jsx index b670c69..2a2a5ef 100644 --- a/web/src/components/Settings.jsx +++ b/web/src/components/Settings.jsx @@ -1,3 +1,4 @@ +import axios from 'axios' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' @@ -11,9 +12,10 @@ import DialogActions from '@material-ui/core/DialogActions' import Button from '@material-ui/core/Button' import { FormControlLabel, InputLabel, Select, Switch } from '@material-ui/core' import { settingsHost, setTorrServerHost, getTorrServerHost } from 'utils/Hosts' -import axios from 'axios' +import { useTranslation } from 'react-i18next' export default function SettingsDialog() { + const { t } = useTranslation() const [open, setOpen] = useState(false) const [settings, setSets] = useState({}) const [show, setShow] = useState(false) @@ -49,7 +51,16 @@ export default function SettingsDialog() { if (type === 'number' || type === 'select-one') { sets[id] = Number(value) } else if (type === 'checkbox') { - sets[id] = Boolean(checked) + if ( + id === 'DisableTCP' || + id === 'DisableUTP' || + id === 'DisableUPNP' || + id === 'DisableDHT' || + id === 'DisablePEX' || + id === 'DisableUpload' + ) + sets[id] = Boolean(!checked) + else sets[id] = Boolean(checked) } else if (type === 'url') { sets[id] = value } @@ -82,21 +93,21 @@ export default function SettingsDialog() { return (
- + - + - Settings + {t('Settings')} - } - label='Preload buffer' - /> +

-
- Retracker mode - - } - label='Enable IPv6' - /> -
- } - label='Force encrypt' - /> -
- } - label='Disable TCP' - /> -
- } - label='Disable UTP' - /> -
- } - label='Disable UPNP' - /> -
- } - label='Disable DHT' - /> -
- } - label='Disable PEX' - /> -
- } - label='Disable upload' - /> -
- - - - - } + label={t('PreloadBuffer')} />
} - label='Use disk' + label={t('UseDisk')} />
} - label='Remove cache from disk on drop torrent' + label={t('RemoveCacheOnDrop')} />
- If disabled, remove cache on delete torrent + {t('RemoveCacheOnDropDesc')}
+
+ } + label={t('EnableIPv6')} + /> +
+ } + label={t('TCP')} + /> +
+ } + label={t('UTP')} + /> +
+ } + label={t('PEX')} + /> +
+ } + label={t('ForceEncrypt')} + /> +
+ +
+ +
+ } + label={t('DHT')} + /> +
+ +
+ +
+ } + label={t('Upload')} + /> +
+ +
+ +
+ } + label={t('UPNP')} + /> +
+ {t('RetrackersMode')} + +
)}
diff --git a/web/src/components/TorrentCard/index.jsx b/web/src/components/TorrentCard/index.jsx index d8c94f1..b9abfa6 100644 --- a/web/src/components/TorrentCard/index.jsx +++ b/web/src/components/TorrentCard/index.jsx @@ -10,12 +10,14 @@ import Slide from '@material-ui/core/Slide' import { Button, DialogActions, DialogTitle, useMediaQuery, useTheme } from '@material-ui/core' import axios from 'axios' import ptt from 'parse-torrent-title' +import { useTranslation } from 'react-i18next' import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardPoster } from './style' const Transition = forwardRef((props, ref) => ) export default function Torrent({ torrent }) { + const { t } = useTranslation() const [isDetailedInfoOpened, setIsDetailedInfoOpened] = useState(false) const [isDeleteTorrentOpened, setIsDeleteTorrentOpened] = useState(false) @@ -44,41 +46,41 @@ export default function Torrent({ torrent }) { - Details + {t('Details')} dropTorrent(torrent)}> - Drop + {t('Drop')} - Delete + {t('Delete')}
-
Name
+
{t('Name')}
{shortenText(parsedTitle, 100)}
-
Size
+
{t('Size')}
{torrentSize > 0 && humanizeSize(torrentSize)}
-
Speed
+
{t('Speed')}
{downloadSpeed > 0 ? humanizeSize(downloadSpeed) : '---'}
-
Peers
+
{t('Peers')}
{getPeerString(torrent) || '---'}
@@ -97,10 +99,10 @@ export default function Torrent({ torrent }) { - Delete Torrent? + {t('DeleteTorrent?')} diff --git a/web/src/components/TorrentCard/style.js b/web/src/components/TorrentCard/style.js index e9da211..7695aba 100644 --- a/web/src/components/TorrentCard/style.js +++ b/web/src/components/TorrentCard/style.js @@ -8,7 +8,7 @@ export const TorrentCard = styled.div` grid-template-areas: 'poster description buttons'; gap: 10px; padding: 10px; - background: #3fb57a; + background: #00a572; box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); @media (max-width: 1260px), (max-height: 500px) { diff --git a/web/src/components/TorrentList.jsx b/web/src/components/TorrentList.jsx index c75c9d2..4168b22 100644 --- a/web/src/components/TorrentList.jsx +++ b/web/src/components/TorrentList.jsx @@ -5,8 +5,10 @@ import TorrentCard from 'components/TorrentCard' import axios from 'axios' import CircularProgress from '@material-ui/core/CircularProgress' import { TorrentListWrapper, CenteredGrid } from 'App/style' +import { useTranslation } from 'react-i18next' export default function TorrentList() { + const { t } = useTranslation() const [torrents, setTorrents] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isOffline, setIsOffline] = useState(true) @@ -39,9 +41,9 @@ export default function TorrentList() { {isLoading ? ( ) : isOffline ? ( - Offline + {t('Offline')} ) : ( - !torrents.length && No torrents added + !torrents.length && {t('NoTorrentsAdded')} )} ) diff --git a/web/src/components/Upload.jsx b/web/src/components/Upload.jsx index 8b1d7ab..8f460d8 100644 --- a/web/src/components/Upload.jsx +++ b/web/src/components/Upload.jsx @@ -4,8 +4,10 @@ import ListItem from '@material-ui/core/ListItem' import PublishIcon from '@material-ui/icons/Publish' import { torrentUploadHost } from 'utils/Hosts' import axios from 'axios' +import { useTranslation } from 'react-i18next' export default function UploadDialog() { + const { t } = useTranslation() const handleCapture = ({ target: { files } }) => { const [file] = files const data = new FormData() @@ -13,18 +15,17 @@ export default function UploadDialog() { data.append('file', file) axios.post(torrentUploadHost(), data) } - return (
diff --git a/web/src/index.jsx b/web/src/index.jsx index 52bdd23..898f403 100644 --- a/web/src/index.jsx +++ b/web/src/index.jsx @@ -1,12 +1,40 @@ import { StrictMode } from 'react' import ReactDOM from 'react-dom' +import { I18nextProvider } from 'react-i18next' +import i18n from 'i18next' +import LanguageDetector from 'i18next-browser-languagedetector' import './index.css' import App from './App' +i18n.use(LanguageDetector).init({ + lng: 'en', // default + fallbackLng: 'en', // use en if detected lng is not available + keySeparator: false, // we do not use keys in form messages.welcome + interpolation: { + escapeValue: false, // react already safes from xss + }, + resources: { + en: { + // eslint-disable-next-line global-require + translations: require('./locales/en/translation.json'), + }, + ru: { + // eslint-disable-next-line global-require + translations: require('./locales/ru/translation.json'), + }, + }, + ns: ['translations'], + defaultNS: 'translations', +}) + +i18n.languages = ['en', 'ru'] + ReactDOM.render( - + + + , document.getElementById('root'), ) diff --git a/web/src/locales/en/translation.json b/web/src/locales/en/translation.json new file mode 100644 index 0000000..c107080 --- /dev/null +++ b/web/src/locales/en/translation.json @@ -0,0 +1,89 @@ +{ + "About": "About", + "Actions": "Actions", + "Add": "Add", + "AddFromLink": "Add from Link", + "AddMagnetOrLink": "Add magnet or link to torrent file", + "AddRetrackers": "Add retrackers", + "Buffer": "Preload Buffer / Cache", + "BufferNote": "Enable "Preload Buffer" in settings to see cache loading progress", + "CacheSize": "Cache Size (Megabytes)", + "Cancel": "Cancel", + "Close": "Close", + "CloseServer": "Close Server", + "CloseServer?": "Close server?", + "ConnectionsLimit": "Connections Limit", + "CopyHash": "Copy Hash", + "CopyLink": "Copy link", + "Delete": "Delete", + "DeleteTorrent?": "Delete Torrent?", + "DeleteTorrents?": "Delete All Torrents?", + "DetailedCacheView": "Detailed Cache View", + "Details": "Details", + "DHT": "DHT (Distributed Hash Table)", + "DhtConnectionLimit": "DHT Connection Limit", + "Donate": "Donate", + "DontAddRetrackers": "Don't add retrackers", + "DownloadPlaylist": "Download Playlist", + "DownloadRateLimit": "Download Rate Limit (Kilobytes)", + "DownloadSpeed": "Download speed", + "Drop": "Drop", + "DropTorrent": "Reset Torrent", + "EnableIPv6": "IPv6", + "Episode": "Episode", + "ForceEncrypt": "Force Encrypt Headers", + "FromLatestFile": "From Latest File", + "Full": "Full", + "Host": "Host", + "Info": "Info", + "LatestFilePlayed": "Latest file played:", + "MagnetOrTorrentFileLink": "Magnet or torrent file link", + "Name": "Name", + "NoTorrentsAdded": "No torrents added", + "Offline": "Offline", + "OK": "OK", + "OpenLink": "Open link", + "Peers": "Peers", + "PeersListenPort": "Peers Listen Port", + "PEX": "PEX (Peer Exchange)", + "PiecesCount": "Pieces count", + "PiecesLength": "Pieces length", + "PlaylistAll": "Playlist All", + "Poster": "Poster", + "Preload": "Preload", + "PreloadBuffer": "Preload Buffer", + "ReaderReadAHead": "Reader Read Ahead (5-100%)", + "RemoveAll": "Remove All", + "RemoveCacheOnDrop": "Remove Cache from Disk on Drop Torrent", + "RemoveCacheOnDropDesc": "If disabled, remove cache on delete torrent.", + "RemoveRetrackers": "Remove retrackers", + "RemoveViews": "Remove View States", + "ReplaceRetrackers": "Replace retrackers", + "Resolution": "Resolution", + "RetrackersMode": "Retrackers Mode", + "Save": "Save", + "Season": "Season", + "SelectSeason": "Select Season", + "Settings": "Settings", + "Size": "Size", + "SpecialThanks": "Special Thanks:", + "Speed": "Speed", + "TCP": "TCP (Transmission Control Protocol)", + "ThanksToEveryone": "Thanks to everyone who tested and helped.", + "Title": "Title", + "TorrentContent": "Torrent Content", + "TorrentDetails": "Torrent Details", + "TorrentDisconnectTimeout": "Torrent Disconnect Timeout", + "TorrentSize": "Torrent size", + "TorrentsSavePath": "Torrents Save Path", + "TorrentState": "Torrent State", + "TorrentStatus": "Torrent Status", + "Upload": "Upload (not recommended to disable)", + "UploadFile": "Upload File", + "UploadRateLimit": "Upload Rate Limit (Kilobytes)", + "UploadSpeed": "Upload speed", + "UPNP": "UPnP (Universal Plug and Play)", + "UseDisk": "Use Disk", + "UTP": "μTP (Micro Transport Protocol)", + "Viewed": "Viewed" +} \ No newline at end of file diff --git a/web/src/locales/ru/translation.json b/web/src/locales/ru/translation.json new file mode 100644 index 0000000..4c7551f --- /dev/null +++ b/web/src/locales/ru/translation.json @@ -0,0 +1,89 @@ +{ + "About": "О сервере", + "Actions": "Действия", + "Add": "Добавить", + "AddFromLink": "Добавить", + "AddMagnetOrLink": "Добавьте magnet или ссылку на торрент", + "AddRetrackers": "Добавлять", + "Buffer": "Предзагрузка / Кеш", + "BufferNote": "Включите «Наполнять кеш перед началом воспроизведения» в настройках для показа заполнения кеша", + "CacheSize": "Размер кеша (Мегабайты)", + "Cancel": "Отмена", + "Close": "Закрыть", + "CloseServer": "Выкл. сервер", + "CloseServer?": "Остановить сервер?", + "ConnectionsLimit": "Торрент-соединения (рек. 20-25)", + "CopyHash": "Скопировать хеш", + "CopyLink": "Копировать", + "Delete": "Удалить", + "DeleteTorrent?": "Удалить торрент?", + "DeleteTorrents?": "Удалить все торренты?", + "DetailedCacheView": "Информация о заполнении кеша", + "Details": "Инфо", + "DHT": "DHT (Distributed Hash Table)", + "DhtConnectionLimit": "Лимит подключений DHT", + "Donate": "Поддержка", + "DontAddRetrackers": "Ничего не делать", + "DownloadPlaylist": "Скачать плейлист", + "DownloadRateLimit": "Ограничение скорости загрузки (Килобайты)", + "DownloadSpeed": "Скорость загрузки", + "Drop": "Сброс", + "DropTorrent": "Сбросить торрент", + "EnableIPv6": "IPv6", + "Episode": "Серия", + "ForceEncrypt": "Принудительное шифрование заголовков", + "FromLatestFile": "C последнего файла", + "Full": "Полный", + "Host": "Хост", + "Info": "Инфо", + "LatestFilePlayed": "Последний воспроизведенный файл:", + "MagnetOrTorrentFileLink": "Ссылка на файл торрента или magnet-ссылка", + "Name": "Название", + "NoTorrentsAdded": "Нет торрентов", + "Offline": "Сервер не доступен", + "OK": "OK", + "OpenLink": "Открыть", + "Peers": "Подкл./Пиры", + "PeersListenPort": "Порт для входящих подключений", + "PEX": "PEX (Peer Exchange)", + "PiecesCount": "Кол-во блоков", + "PiecesLength": "Размер блока", + "PlaylistAll": "Плейлист всех", + "Poster": "Постер", + "Preload": "Предзагр.", + "PreloadBuffer": "Наполнять кеш перед началом воспроизведения", + "ReaderReadAHead": "Кеш предзагрузки (5-100%, рек. 95%)", + "RemoveAll": "Удалить все", + "RemoveCacheOnDrop": "Очищать кеш на диске при отключении торрента", + "RemoveCacheOnDropDesc": "Если отключено, кэш очищается при удалении торрента.", + "RemoveRetrackers": "Удалять", + "RemoveViews": "Очистить просмотры", + "ReplaceRetrackers": "Заменять", + "Resolution": "Разреш.", + "RetrackersMode": "Ретрекеры", + "Save": "Сохранить", + "Season": "Сезон", + "SelectSeason": "Выбор сезона", + "Settings": "Настройки", + "Size": "Размер", + "SpecialThanks": "Отдельное спасибо:", + "Speed": "Скорость", + "TCP": "TCP (Transmission Control Protocol)", + "ThanksToEveryone": "Спасибо всем, кто тестировал и помогал!", + "Title": "Название", + "TorrentContent": "Содержимое торрента", + "TorrentDetails": "Информация о торренте", + "TorrentDisconnectTimeout": "Тайм-аут отключения торрента (секунды)", + "TorrentSize": "Размер торрента", + "TorrentsSavePath": "Путь хранения кеша", + "TorrentState": "Данные торрента", + "TorrentStatus": "Состояние", + "Upload": "Отдача (не рекомендуется отключать)", + "UploadFile": "Загрузить файл", + "UploadRateLimit": "Ограничение скорости отдачи (Килобайты)", + "UploadSpeed": "Скорость отдачи", + "UPNP": "UPnP (Universal Plug and Play)", + "UseDisk": "Использовать кеш на диске", + "UTP": "μTP (Micro Transport Protocol)", + "Viewed": "Просм." +} \ No newline at end of file