diff --git a/web/package.json b/web/package.json index 958db46..70e37dd 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,9 @@ "axios": "^0.21.1", "clsx": "^1.1.1", "fontsource-roboto": "^4.0.0", + "i18next": "^20.3.1", + "i18next-browser-languagedetector": "^6.1.1", + "i18next-xhr-backend": "^3.2.2", "konva": "^8.0.1", "lodash": "^4.17.21", "material-ui-image": "^3.3.2", @@ -16,6 +19,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 5d6edac..1d792e0 100644 --- a/web/src/App/Sidebar.jsx +++ b/web/src/App/Sidebar.jsx @@ -14,21 +14,24 @@ import { PowerSettingsNew as PowerSettingsNewIcon, } from '@material-ui/icons' import List from '@material-ui/core/List' +import { useTranslation } from 'react-i18next' import { AppSidebarStyle } from './style' export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') return ( - + - + @@ -37,11 +40,11 @@ export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { - fetch(shutdownHost())}> + fetch(shutdownHost())}> - + @@ -52,7 +55,7 @@ export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { - + diff --git a/web/src/components/About.jsx b/web/src/components/About.jsx index a241eee..cfc5894 100644 --- a/web/src/components/About.jsx +++ b/web/src/components/About.jsx @@ -8,36 +8,38 @@ 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' export default function AboutDialog() { const [open, setOpen] = useState(false) - + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') return (
setOpen(true)}> - + setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'> - About + {t('About')}
-

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 +48,7 @@ export default function AboutDialog() {
diff --git a/web/src/components/Add/index.jsx b/web/src/components/Add/index.jsx index b798785..7e27629 100644 --- a/web/src/components/Add/index.jsx +++ b/web/src/components/Add/index.jsx @@ -3,22 +3,23 @@ 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 [isDialogOpen, setIsDialogOpen] = useState(false) - const handleClickOpen = () => setIsDialogOpen(true) const handleClose = () => setIsDialogOpen(false) - + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') return (
- + {isDialogOpen && } diff --git a/web/src/components/Donate/DonateDialog.jsx b/web/src/components/Donate/DonateDialog.jsx index 40a0b75..5d0a113 100644 --- a/web/src/components/Donate/DonateDialog.jsx +++ b/web/src/components/Donate/DonateDialog.jsx @@ -6,14 +6,17 @@ import DialogActions from '@material-ui/core/DialogActions' import List from '@material-ui/core/List' import ButtonGroup from '@material-ui/core/ButtonGroup' import Button from '@material-ui/core/Button' +import { useTranslation } from 'react-i18next' const donateFrame = '' export default function DonateDialog({ onClose }) { + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') return ( - Donate + {t('Donate')} diff --git a/web/src/components/RemoveAll.jsx b/web/src/components/RemoveAll.jsx index aac5c0f..53705fe 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(), { @@ -34,22 +35,23 @@ export default function RemoveAll() { const [open, setOpen] = useState(false) const closeDialog = () => setOpen(false) const openDialog = () => setOpen(true) - + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') return ( <> - + - + - Delete Torrent? + {t('DeleteTorrents?')} diff --git a/web/src/components/Settings.jsx b/web/src/components/Settings.jsx index b670c69..c6af72e 100644 --- a/web/src/components/Settings.jsx +++ b/web/src/components/Settings.jsx @@ -12,6 +12,7 @@ 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 [open, setOpen] = useState(false) @@ -27,6 +28,8 @@ export default function SettingsDialog() { sets.CacheSize *= 1024 * 1024 axios.post(settingsHost(), { action: 'set', sets }) } + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') useEffect(() => { axios @@ -82,21 +85,21 @@ export default function SettingsDialog() { return (
- + - + - Settings + {t('Settings')} } - label='Preload buffer' + label={t('PreloadBuffer')} />

- Retracker mode + {t('RetrackersMode')} } - label='Enable IPv6' + label={t('EnableIPv6')} />
} - label='Force encrypt' + label={t('ForceEncrypt')} />
} - label='Disable TCP' + label={t('DisableTCP')} />
} - label='Disable UTP' + label={t('DisableUTP')} />
} - label='Disable UPNP' + label={t('DisableUPNP')} />
} - label='Disable DHT' + label={t('DisableDHT')} />
} - label='Disable PEX' + label={t('DisablePEX')} />
} - label='Disable upload' + label={t('DisableUpload')} />
} - 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')}
diff --git a/web/src/components/TorrentCard/index.jsx b/web/src/components/TorrentCard/index.jsx index d8c94f1..0580d88 100644 --- a/web/src/components/TorrentCard/index.jsx +++ b/web/src/components/TorrentCard/index.jsx @@ -10,12 +10,15 @@ 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 }) { + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') const [isDetailedInfoOpened, setIsDetailedInfoOpened] = useState(false) const [isDeleteTorrentOpened, setIsDeleteTorrentOpened] = useState(false) @@ -44,41 +47,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 +100,10 @@ export default function Torrent({ torrent }) {
- Delete Torrent? + {t('DeleteTorrent?')} diff --git a/web/src/components/Upload.jsx b/web/src/components/Upload.jsx index 8b1d7ab..e1dcfdc 100644 --- a/web/src/components/Upload.jsx +++ b/web/src/components/Upload.jsx @@ -4,6 +4,7 @@ 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 handleCapture = ({ target: { files } }) => { @@ -13,18 +14,19 @@ export default function UploadDialog() { data.append('file', file) axios.post(torrentUploadHost(), data) } - + // eslint-disable-next-line no-unused-vars + const { t, i18n } = useTranslation('translations') return (
diff --git a/web/src/index.jsx b/web/src/index.jsx index 52bdd23..692b8f8 100644 --- a/web/src/index.jsx +++ b/web/src/index.jsx @@ -1,12 +1,42 @@ 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 XHR from 'i18next-xhr-backend' import './index.css' import App from './App' +import translationEng from './locales/en/translation.json' +import translationRus from './locales/ru/translation.json' + +i18n + .use(XHR) + .use(LanguageDetector) + .init({ + lng: 'ru', // 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: { + translations: translationEng, + }, + ru: { + translations: translationRus, + }, + }, + ns: ['translations'], + defaultNS: 'translations', + }) 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..b788b7b --- /dev/null +++ b/web/src/locales/en/translation.json @@ -0,0 +1,52 @@ +{ + "About": "About", + "AddFromLink": "Add from Link", + "CacheSize": "Cache Size (Megabytes)", + "Cancel": "Cancel", + "Close": "Close", + "CloseServer": "Close Server", + "ConnectionsLimit": "Connections Limit", + "Delete": "Delete", + "DeleteTorrent?": "Delete Torrent?", + "DeleteTorrents?": "Delete All Torrents?", + "Details": "Details", + "DhtConnectionLimit": "DHT Connection Limit", + "DisableDHT": "Disable DHT", + "DisablePEX": "Disable PEX", + "DisableTCP": "Disable TCP", + "DisableUpload": "Disable Upload", + "DisableUPNP": "Disable UPNP", + "DisableUTP": "Disable UTP", + "Donate": "Donate", + "DownloadRateLimit": "Download Rate Limit", + "Drop": "Drop", + "EnableIPv6": "Enable IPv6", + "ForceEncrypt": "Force Encrypt Headers", + "Host": "Host", + "Name": "Name", + "OK": "OK", + "Peers": "Peers", + "PeersListenPort": "Peers Listen Port", + "PlaylistAll": "Playlist All", + "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.", + "RetrackersMode": "Retrackers Mode", + "DontAddRetrackers": "Don't add retrackers", + "AddRetrackers": "Add retrackers", + "RemoveRetrackers": "Remove retrackers", + "ReplaceRetrackers": "Replace retrackers", + "Save": "Save", + "Settings": "Settings", + "Size": "Size", + "SpecialThanks": "Special Thanks:", + "Speed": "Speed", + "ThanksToEveryone": "Thanks to everyone who tested and helped.", + "TorrentDisconnectTimeout": "Torrent Disconnect Timeout", + "TorrentsSavePath": "Torrents Save Path", + "UploadFile": "Upload File", + "UploadRateLimit": "Upload Rate Limit", + "UseDisk": "Use Disk" +} \ 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..2620d5c --- /dev/null +++ b/web/src/locales/ru/translation.json @@ -0,0 +1,52 @@ +{ + "About": "О сервере", + "AddFromLink": "Добавить", + "CacheSize": "Размер кеша (Мегабайты)", + "Cancel": "Отмена", + "Close": "Закрыть", + "CloseServer": "Выкл. сервер", + "ConnectionsLimit": "Торрент-соединения", + "Delete": "Удалить", + "DeleteTorrent?": "Удалить торрент?", + "DeleteTorrents?": "Удалить все торренты?", + "Details": "Информация", + "DhtConnectionLimit": "Лимит подключений DHT", + "DisableDHT": "Откл. DHT", + "DisablePEX": "Откл. PEX", + "DisableTCP": "Откл. TCP", + "DisableUpload": "Откл. отдачу", + "DisableUPNP": "Откл. UPNP", + "DisableUTP": "Откл. UTP", + "Donate": "Поддержка", + "DownloadRateLimit": "Ограничение скорости загрузки", + "Drop": "Отключить", + "EnableIPv6": "Вкл. IPv6", + "ForceEncrypt": "Принудительное шифрование заголовков", + "Host": "Хост", + "Name": "Имя", + "OK": "OK", + "Peers": "Подключения", + "PeersListenPort": "Порт для входящих подключений", + "PlaylistAll": "Плейлист всех", + "PreloadBuffer": "Наполнять кеш перед началом воспроизведения", + "ReaderReadAHead": "Кеш предзагрузки (5-100%, рек.95%)", + "RemoveAll": "Удалить все", + "RemoveCacheOnDrop": "Очищать кеш на диске при отключении торрента", + "RemoveCacheOnDropDesc": "Если отключено, кеш очищается при удалении торрента.", + "RetrackersMode": "Ретрекеры", + "DontAddRetrackers": "Ничего не делать", + "AddRetrackers": "Добавлять", + "RemoveRetrackers": "Удалять", + "ReplaceRetrackers": "Заменять", + "Save": "Сохранить", + "Settings": "Настройки", + "Size": "Размер", + "SpecialThanks": "Отдельное спасибо:", + "Speed": "Скорость", + "ThanksToEveryone": "Спасибо всем, кто тестировал и помогал!", + "TorrentDisconnectTimeout": "Тайм-аут отключения торрента (секунды)", + "TorrentsSavePath": "Путь хранения кеша", + "UploadFile": "Загрузить файл", + "UploadRateLimit": "Ограничение скорости отдачи", + "UseDisk": "Использовать кеш на диске" +} \ No newline at end of file