add translation keys (#246)
* Fix i18n tsc issue "t" can return null by default. This was already disabled in the config, but it had no effect on tsc. * Fix translation in "DefaultNavBar" key was missing and since this is a constant the translation wouldn't update in case the language got changed after first load. * Enable typing for i18n translation keys * Add translation keys
This commit is contained in:
@@ -14,7 +14,8 @@ import Typography from '@mui/material/Typography';
|
|||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { IExtension } from 'typings';
|
import { IExtension, TranslationKey } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
extension: IExtension;
|
extension: IExtension;
|
||||||
@@ -50,7 +51,19 @@ const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]: Extens
|
|||||||
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
|
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const INSTALLED_STATE_TO_TRANSLATION_KEY_MAP: { [installedState in InstalledStates]: TranslationKey } = {
|
||||||
|
[InstalledState.UNINSTALL]: 'extension.action.label.uninstall',
|
||||||
|
[InstalledState.INSTALL]: 'extension.action.label.install',
|
||||||
|
[InstalledState.UPDATE]: 'extension.action.label.update',
|
||||||
|
[InstalledState.OBSOLETE]: 'extension.state.label.obsolete',
|
||||||
|
[InstalledState.UPDATING]: 'extension.state.label.updating',
|
||||||
|
[InstalledState.UNINSTALLING]: 'extension.state.label.uninstalling',
|
||||||
|
[InstalledState.INSTALLING]: 'extension.state.label.installing',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function ExtensionCard(props: IProps) {
|
export default function ExtensionCard(props: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
extension: { name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw },
|
extension: { name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw },
|
||||||
notifyInstall,
|
notifyInstall,
|
||||||
@@ -68,7 +81,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||||
|
|
||||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
const langPress = lang === 'all' ? t('extension.language.all') : lang.toUpperCase();
|
||||||
|
|
||||||
const requestExtensionAction = async (action: ExtensionAction): Promise<void> => {
|
const requestExtensionAction = async (action: ExtensionAction): Promise<void> => {
|
||||||
const nextAction = EXTENSION_ACTION_TO_NEXT_ACTION_MAP[action];
|
const nextAction = EXTENSION_ACTION_TO_NEXT_ACTION_MAP[action];
|
||||||
@@ -137,7 +150,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
sx={{ color: installedState === InstalledState.OBSOLETE ? 'red' : 'inherit' }}
|
sx={{ color: installedState === InstalledState.OBSOLETE ? 'red' : 'inherit' }}
|
||||||
onClick={() => handleButtonClick()}
|
onClick={() => handleButtonClick()}
|
||||||
>
|
>
|
||||||
{installedState}
|
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Box, styled } from '@mui/system';
|
|||||||
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
import { BACK } from 'util/useBackTo';
|
import { BACK } from 'util/useBackTo';
|
||||||
import { IMangaCard } from 'typings';
|
import { IMangaCard } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const BottomGradient = styled('div')({
|
const BottomGradient = styled('div')({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -73,6 +74,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
|
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
manga: {
|
manga: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@@ -131,7 +134,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
>
|
>
|
||||||
{inLibraryIndicator && inLibrary && (
|
{inLibraryIndicator && inLibrary && (
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}>
|
<Typography sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}>
|
||||||
In library
|
{t('manga.button.in_library')}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{showUnreadBadge && unread! > 0 && (
|
{showUnreadBadge && unread! > 0 && (
|
||||||
@@ -252,7 +255,9 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
</Box>
|
</Box>
|
||||||
<BadgeContainer>
|
<BadgeContainer>
|
||||||
{inLibraryIndicator && inLibrary && (
|
{inLibraryIndicator && inLibrary && (
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>In library</Typography>
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
|
{t('manga.button.in_library')}
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{showUnreadBadge && unread! > 0 && (
|
{showUnreadBadge && unread! > 0 && (
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>{unread}</Typography>
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>{unread}</Typography>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import CardContent from '@mui/material/CardContent';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Box, styled } from '@mui/system';
|
import { Box, styled } from '@mui/system';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { langCodeToName } from 'util/language';
|
import { langCodeToName } from 'util/language';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
@@ -41,6 +42,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SourceCard: React.FC<IProps> = (props: IProps) => {
|
const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
source: { id, name, lang, iconUrl, supportsLatest, isNsfw },
|
source: { id, name, lang, iconUrl, supportsLatest, isNsfw },
|
||||||
} = props;
|
} = props;
|
||||||
@@ -110,18 +113,18 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<MobileWidthButtons>
|
<MobileWidthButtons>
|
||||||
{supportsLatest && (
|
{supportsLatest && (
|
||||||
<Button variant="outlined" onClick={(e) => redirectTo(e, `/sources/${id}/latest/`)}>
|
<Button variant="outlined" onClick={(e) => redirectTo(e, `/sources/${id}/latest/`)}>
|
||||||
Latest
|
{t('global.button.latest')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</MobileWidthButtons>
|
</MobileWidthButtons>
|
||||||
<WiderWidthButtons>
|
<WiderWidthButtons>
|
||||||
{supportsLatest && (
|
{supportsLatest && (
|
||||||
<Button component={Link} to={`/sources/${id}/latest/`} variant="outlined">
|
<Button component={Link} to={`/sources/${id}/latest/`} variant="outlined">
|
||||||
Latest
|
{t('global.button.latest')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button component={Link} to={`/sources/${id}/popular/`} variant="outlined">
|
<Button component={Link} to={`/sources/${id}/popular/`} variant="outlined">
|
||||||
Browse
|
{t('global.button.browse')}
|
||||||
</Button>
|
</Button>
|
||||||
</WiderWidthButtons>
|
</WiderWidthButtons>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import { StringParam, useQueryParam } from 'use-query-params';
|
|||||||
import { useMediaQuery, useTheme } from '@mui/material';
|
import { useMediaQuery, useTheme } from '@mui/material';
|
||||||
import { IMangaCard, LibrarySortMode, NullAndUndefined } from 'typings';
|
import { IMangaCard, LibrarySortMode, NullAndUndefined } from 'typings';
|
||||||
import { useSearchSettings } from 'util/searchSettings';
|
import { useSearchSettings } from 'util/searchSettings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
const FILTERED_OUT_MESSAGE = 'There are no Manga matching this filter';
|
|
||||||
|
|
||||||
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IMangaCard): boolean => {
|
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IMangaCard): boolean => {
|
||||||
switch (unread) {
|
switch (unread) {
|
||||||
@@ -118,6 +117,8 @@ const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: nu
|
|||||||
message,
|
message,
|
||||||
lastLibraryUpdate,
|
lastLibraryUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
const { options } = useLibraryOptionsContext();
|
const { options } = useLibraryOptionsContext();
|
||||||
const { unread, downloaded } = options;
|
const { unread, downloaded } = options;
|
||||||
@@ -155,7 +156,7 @@ const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: nu
|
|||||||
hasNextPage={lastPageNum < totalPages}
|
hasNextPage={lastPageNum < totalPages}
|
||||||
lastPageNum={lastPageNum}
|
lastPageNum={lastPageNum}
|
||||||
setLastPageNum={setLastPageNum}
|
setLastPageNum={setLastPageNum}
|
||||||
message={showFilteredOutMessage ? FILTERED_OUT_MESSAGE : message}
|
message={showFilteredOutMessage ? t('library.error.label.no_matches') : message}
|
||||||
gridLayout={options.gridLayout}
|
gridLayout={options.gridLayout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,19 +13,20 @@ import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
|||||||
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
import OptionsTabs from 'components/molecules/OptionsTabs';
|
import OptionsTabs from 'components/molecules/OptionsTabs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LibraryOptions, LibrarySortMode } from 'typings';
|
import { LibraryOptions, LibrarySortMode, TranslationKey } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const TITLES = {
|
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||||
filter: 'Filter',
|
filter: 'global.label.filter',
|
||||||
sort: 'Sort',
|
sort: 'global.label.sort',
|
||||||
display: 'Display',
|
display: 'global.label.display',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SORT_OPTIONS: [LibrarySortMode, string][] = [
|
const SORT_OPTIONS: [LibrarySortMode, TranslationKey][] = [
|
||||||
['sortToRead', 'By Unread chapters'],
|
['sortToRead', 'library.option.sort.label.by_unread_chapters'],
|
||||||
['sortAlph', 'Alphabetically'],
|
['sortAlph', 'library.option.sort.label.alphabetically'],
|
||||||
['sortDateAdded', 'Date Added'],
|
['sortDateAdded', 'library.option.sort.label.by_date_added'],
|
||||||
['sortLastRead', 'Last Read'],
|
['sortLastRead', 'library.option.sort.label.by_last_read'],
|
||||||
];
|
];
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -34,6 +35,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { options, setOptions } = useLibraryOptionsContext();
|
const { options, setOptions } = useLibraryOptionsContext();
|
||||||
|
|
||||||
const handleFilterChange = <T extends keyof LibraryOptions>(key: T, value: LibraryOptions[T]) => {
|
const handleFilterChange = <T extends keyof LibraryOptions>(key: T, value: LibraryOptions[T]) => {
|
||||||
@@ -45,18 +47,18 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
tabs={['filter', 'sort', 'display']}
|
tabs={['filter', 'sort', 'display']}
|
||||||
tabTitle={(key) => TITLES[key]}
|
tabTitle={(key) => t(TITLES[key])}
|
||||||
tabContent={(key) => {
|
tabContent={(key) => {
|
||||||
if (key === 'filter') {
|
if (key === 'filter') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ThreeStateCheckboxInput
|
<ThreeStateCheckboxInput
|
||||||
label="Unread"
|
label={t('global.filter.label.unread')}
|
||||||
checked={options.unread}
|
checked={options.unread}
|
||||||
onChange={(c) => handleFilterChange('unread', c)}
|
onChange={(c) => handleFilterChange('unread', c)}
|
||||||
/>
|
/>
|
||||||
<ThreeStateCheckboxInput
|
<ThreeStateCheckboxInput
|
||||||
label="Downloaded"
|
label={t('global.filter.label.downloaded')}
|
||||||
checked={options.downloaded}
|
checked={options.downloaded}
|
||||||
onChange={(c) => handleFilterChange('downloaded', c)}
|
onChange={(c) => handleFilterChange('downloaded', c)}
|
||||||
/>
|
/>
|
||||||
@@ -67,7 +69,7 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
return SORT_OPTIONS.map(([mode, label]) => (
|
return SORT_OPTIONS.map(([mode, label]) => (
|
||||||
<SortRadioInput
|
<SortRadioInput
|
||||||
key={mode}
|
key={mode}
|
||||||
label={label}
|
label={t(label) as string}
|
||||||
checked={options.sorts === mode}
|
checked={options.sorts === mode}
|
||||||
sortDescending={options.sortDesc}
|
sortDescending={options.sortDesc}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -82,36 +84,36 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
const { gridLayout, showDownloadBadge, showUnreadBadge } = options;
|
const { gridLayout, showDownloadBadge, showUnreadBadge } = options;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormLabel>Display mode</FormLabel>
|
<FormLabel>{t('global.grid_layout.title')}</FormLabel>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))}
|
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))}
|
||||||
value={gridLayout}
|
value={gridLayout}
|
||||||
>
|
>
|
||||||
<RadioInput
|
<RadioInput
|
||||||
label="Compact grid"
|
label={t('global.grid_layout.label.compact_grid')}
|
||||||
value={GridLayout.Compact}
|
value={GridLayout.Compact}
|
||||||
checked={gridLayout == null || gridLayout === GridLayout.Compact}
|
checked={gridLayout == null || gridLayout === GridLayout.Compact}
|
||||||
/>
|
/>
|
||||||
<RadioInput
|
<RadioInput
|
||||||
label="Comfortable grid"
|
label={t('global.grid_layout.label.comfortable_grid')}
|
||||||
value={GridLayout.Comfortable}
|
value={GridLayout.Comfortable}
|
||||||
checked={gridLayout === GridLayout.Comfortable}
|
checked={gridLayout === GridLayout.Comfortable}
|
||||||
/>
|
/>
|
||||||
<RadioInput
|
<RadioInput
|
||||||
label="List"
|
label={t('global.grid_layout.label.list')}
|
||||||
value={GridLayout.List}
|
value={GridLayout.List}
|
||||||
checked={gridLayout === GridLayout.List}
|
checked={gridLayout === GridLayout.List}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
|
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.badge.title')}</FormLabel>
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
label="Unread Badges"
|
label={t('library.option.display.badge.label.unread_badges')}
|
||||||
checked={showUnreadBadge === true}
|
checked={showUnreadBadge === true}
|
||||||
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)}
|
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)}
|
||||||
/>
|
/>
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
label="Download Badges"
|
label={t('library.option.display.badge.label.download_badges')}
|
||||||
checked={showDownloadBadge === true}
|
checked={showDownloadBadge === true}
|
||||||
onChange={() => handleFilterChange('showDownloadBadge', !showDownloadBadge)}
|
onChange={() => handleFilterChange('showDownloadBadge', !showDownloadBadge)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Typography from '@mui/material/Typography';
|
|||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
import { IUpdateStatus } from 'typings';
|
import { IUpdateStatus } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProgressProps {
|
interface IProgressProps {
|
||||||
progress: number;
|
progress: number;
|
||||||
@@ -30,6 +31,8 @@ interface IUpdateCheckerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
|||||||
setProgress(0);
|
setProgress(0);
|
||||||
await client.post('/api/v1/update/fetch');
|
await client.post('/api/v1/update/fetch');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
makeToast('Checking for updates failed!', 'error');
|
makeToast(t('global.error.label.update_failed'), 'error');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import client from 'util/client';
|
|||||||
import { BACK } from 'util/useBackTo';
|
import { BACK } from 'util/useBackTo';
|
||||||
import { getUploadDateString } from 'util/date';
|
import { getUploadDateString } from 'util/date';
|
||||||
import { IChapter, IDownloadChapter } from 'typings';
|
import { IChapter, IDownloadChapter } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
chapter: IChapter;
|
chapter: IChapter;
|
||||||
@@ -41,6 +42,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
const { chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
||||||
@@ -135,12 +137,12 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showChapterNumber ? `Chapter ${chapter.chapterNumber}` : chapter.name}
|
{showChapterNumber ? `${t('chapter.title')} ${chapter.chapterNumber}` : chapter.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{getUploadDateString(chapter.uploadDate)}
|
{getUploadDateString(chapter.uploadDate)}
|
||||||
{isDownloaded && ' • Downloaded'}
|
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -160,14 +162,14 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<CheckBoxOutlineBlank fontSize="small" />
|
<CheckBoxOutlineBlank fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Select</ListItemText>
|
<ListItemText>{t('chapter.action.label.select')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{isDownloaded && (
|
{isDownloaded && (
|
||||||
<MenuItem onClick={deleteChapter}>
|
<MenuItem onClick={deleteChapter}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Delete fontSize="small" />
|
<Delete fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Delete</ListItemText>
|
<ListItemText>{t('chapter.action.download.delete.label.action')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{canBeDownloaded && (
|
{canBeDownloaded && (
|
||||||
@@ -175,7 +177,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Download fontSize="small" />
|
<Download fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Download</ListItemText>
|
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||||
@@ -184,8 +186,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />}
|
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
{chapter.bookmarked && 'Remove bookmark'}
|
{chapter.bookmarked && t('chapter.action.bookmark.remove.label.action')}
|
||||||
{!chapter.bookmarked && 'Add bookmark'}
|
{!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
|
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
|
||||||
@@ -194,15 +196,15 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
{!chapter.read && <Done fontSize="small" />}
|
{!chapter.read && <Done fontSize="small" />}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
{chapter.read && 'Mark as unread'}
|
{chapter.read && t('chapter.action.mark_as_read.remove.label.action')}
|
||||||
{!chapter.read && 'Mark as read'}
|
{!chapter.read && t('chapter.action.mark_as_read.add.label.action.current')}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DoneAll fontSize="small" />
|
<DoneAll fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Mark previous as Read</ListItemText>
|
<ListItemText>{t('chapter.action.mark_as_read.add.label.action.previous')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import ChapterCard from 'components/manga/ChapterCard';
|
|||||||
import ResumeFab from 'components/manga/ResumeFAB';
|
import ResumeFab from 'components/manga/ResumeFAB';
|
||||||
import { filterAndSortChapters, useChapterOptions } from 'components/manga/util';
|
import { filterAndSortChapters, useChapterOptions } from 'components/manga/util';
|
||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import { interpolate } from 'components/util/helpers';
|
|
||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
|
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
|
||||||
import SelectionFAB from 'components/manga/SelectionFAB';
|
import SelectionFAB from 'components/manga/SelectionFAB';
|
||||||
import { BatchChaptersChange, IChapter, IDownloadChapter, IQueue } from 'typings';
|
import { BatchChaptersChange, IChapter, IDownloadChapter, IQueue, TranslationKey } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
||||||
listStyle: 'none',
|
listStyle: 'none',
|
||||||
@@ -34,30 +34,35 @@ const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const actionsStrings = {
|
const actionsStrings: {
|
||||||
|
[key in 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread']: {
|
||||||
|
success: TranslationKey;
|
||||||
|
error: TranslationKey;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
download: {
|
download: {
|
||||||
success: { one: 'Download added', many: '%count% downloads added' },
|
success: 'chapter.action.download.add.label.success',
|
||||||
error: { one: 'Error adding download', many: 'Error adding downloads' },
|
error: 'chapter.action.download.add.label.error',
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
success: { one: 'Chapter deleted', many: '%count% chapters deleted' },
|
success: 'chapter.action.download.delete.label.success',
|
||||||
error: { one: 'Error deleting chapter', many: 'Error deleting chapters' },
|
error: 'chapter.action.download.delete.label.error',
|
||||||
},
|
},
|
||||||
bookmark: {
|
bookmark: {
|
||||||
success: { one: 'Chapter bookmarked', many: '%count% chapters bookmarked' },
|
success: 'chapter.action.bookmark.add.label.success',
|
||||||
error: { one: 'Error bookmarking chapter', many: 'Error bookmarking chapters' },
|
error: 'chapter.action.bookmark.add.label.error',
|
||||||
},
|
},
|
||||||
unbookmark: {
|
unbookmark: {
|
||||||
success: { one: 'Chapter bookmark removed', many: '%count% chapter bookmarks removed' },
|
success: 'chapter.action.bookmark.remove.label.success',
|
||||||
error: { one: 'Error removing bookmark', many: 'Error removing bookmarks' },
|
error: 'chapter.action.bookmark.remove.label.error',
|
||||||
},
|
},
|
||||||
mark_as_read: {
|
mark_as_read: {
|
||||||
success: { one: 'Chapter marked as read', many: '%count% chapters marked as read' },
|
success: 'chapter.action.mark_as_read.add.label.success',
|
||||||
error: { one: 'Error marking chapter as read', many: 'Error marking chapters as read' },
|
error: 'chapter.action.mark_as_read.add.label.error',
|
||||||
},
|
},
|
||||||
mark_as_unread: {
|
mark_as_unread: {
|
||||||
success: { one: 'Chapter marked as unread', many: '%count% chapters marked as unread' },
|
success: 'chapter.action.mark_as_read.remove.label.success',
|
||||||
error: { one: 'Error marking chapter as unread', many: 'Error marking chapters as unread' },
|
error: 'chapter.action.mark_as_read.remove.label.error',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +77,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [selection, setSelection] = useState<number[] | null>(null);
|
const [selection, setSelection] = useState<number[] | null>(null);
|
||||||
const prevQueueRef = useRef<IDownloadChapter[]>();
|
const prevQueueRef = useRef<IDownloadChapter[]>();
|
||||||
const queue = useSubscription<IQueue>('/api/v1/downloads').data?.queue;
|
const queue = useSubscription<IQueue>('/api/v1/downloads').data?.queue;
|
||||||
@@ -162,9 +169,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionPromise
|
actionPromise
|
||||||
.then(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].success), 'success'))
|
.then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }) as string, 'success'))
|
||||||
.then(() => mutate())
|
.then(() => mutate())
|
||||||
.catch(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].error), 'error'));
|
.catch(() => makeToast(t(actionsStrings[action].error, { count: chapterIds.length }) as string, 'error'));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -214,7 +221,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
{`${visibleChapters.length} Chapter${visibleChapters.length === 1 ? '' : 's'}`}
|
{`${visibleChapters.length} ${t('chapter.title', {
|
||||||
|
count: visibleChapters.length,
|
||||||
|
})}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{selection === null ? (
|
{selection === null ? (
|
||||||
@@ -222,17 +231,17 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
|||||||
) : (
|
) : (
|
||||||
<Stack direction="row">
|
<Stack direction="row">
|
||||||
<Button size="small" onClick={handleSelectAll}>
|
<Button size="small" onClick={handleSelectAll}>
|
||||||
Select all
|
{t('global.button.select_all')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" onClick={handleClear}>
|
<Button size="small" onClick={handleClear}>
|
||||||
Clear
|
{t('global.button.clear')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{noChaptersFound && <EmptyView message="No chapters found" />}
|
{noChaptersFound && <EmptyView message={t('chapter.error.label.no_chapter_found')} />}
|
||||||
{noChaptersMatchingFilter && <EmptyView message="No chapters matching filter" />}
|
{noChaptersMatchingFilter && <EmptyView message={t('chapter.error.label.no_matches')} />}
|
||||||
|
|
||||||
<StyledVirtuoso
|
<StyledVirtuoso
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
|||||||
import OptionsTabs from 'components/molecules/OptionsTabs';
|
import OptionsTabs from 'components/molecules/OptionsTabs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SORT_OPTIONS } from 'components/manga/util';
|
import { SORT_OPTIONS } from 'components/manga/util';
|
||||||
import { ChapterListOptions, ChapterOptionsReducerAction } from 'typings';
|
import { ChapterListOptions, ChapterOptionsReducerAction, TranslationKey } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -21,25 +22,28 @@ interface IProps {
|
|||||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TITLES = {
|
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||||
filter: 'Filter',
|
filter: 'global.label.filter',
|
||||||
sort: 'Sort',
|
sort: 'global.label.sort',
|
||||||
display: 'Display',
|
display: 'global.label.display',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => (
|
const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
minHeight={150}
|
minHeight={150}
|
||||||
tabs={['filter', 'sort', 'display']}
|
tabs={['filter', 'sort', 'display']}
|
||||||
tabTitle={(key) => TITLES[key]}
|
tabTitle={(key) => t(TITLES[key])}
|
||||||
tabContent={(key) => {
|
tabContent={(key) => {
|
||||||
if (key === 'filter') {
|
if (key === 'filter') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ThreeStateCheckboxInput
|
<ThreeStateCheckboxInput
|
||||||
label="Unread"
|
label={t('global.filter.label.unread')}
|
||||||
checked={options.unread}
|
checked={options.unread}
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
optionsDispatch({
|
optionsDispatch({
|
||||||
@@ -50,7 +54,7 @@ const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispa
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ThreeStateCheckboxInput
|
<ThreeStateCheckboxInput
|
||||||
label="Downloaded"
|
label={t('global.filter.label.downloaded')}
|
||||||
checked={options.downloaded}
|
checked={options.downloaded}
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
optionsDispatch({
|
optionsDispatch({
|
||||||
@@ -61,7 +65,7 @@ const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispa
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ThreeStateCheckboxInput
|
<ThreeStateCheckboxInput
|
||||||
label="Bookmarked"
|
label={t('global.filter.label.bookmarked')}
|
||||||
checked={options.bookmarked}
|
checked={options.bookmarked}
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
optionsDispatch({
|
optionsDispatch({
|
||||||
@@ -78,7 +82,7 @@ const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispa
|
|||||||
return SORT_OPTIONS.map(([mode, label]) => (
|
return SORT_OPTIONS.map(([mode, label]) => (
|
||||||
<SortRadioInput
|
<SortRadioInput
|
||||||
key={mode}
|
key={mode}
|
||||||
label={label}
|
label={t(label) as string}
|
||||||
checked={options.sortBy === mode}
|
checked={options.sortBy === mode}
|
||||||
sortDescending={options.reverse}
|
sortDescending={options.reverse}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -95,8 +99,8 @@ const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispa
|
|||||||
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
|
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
|
||||||
value={options.showChapterNumber}
|
value={options.showChapterNumber}
|
||||||
>
|
>
|
||||||
<RadioInput label="Source Title" value={false} />
|
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} />
|
||||||
<RadioInput label="Chapter Number" value />
|
<RadioInput label={t('chapter.option.display.label.chapter_number')} value />
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,5 +108,6 @@ const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispa
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ChapterOptions;
|
export default ChapterOptions;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr';
|
||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
@@ -129,6 +130,8 @@ function getValueOrUnknown(val: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||||
|
|
||||||
@@ -158,15 +161,15 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
<div className={classes.rightSide}>
|
<div className={classes.rightSide}>
|
||||||
<h1>{manga.title}</h1>
|
<h1>{manga.title}</h1>
|
||||||
<h3>
|
<h3>
|
||||||
{'Author: '}
|
{`${t('manga.label.author')}: `}
|
||||||
<span>{getValueOrUnknown(manga.author)}</span>
|
<span>{getValueOrUnknown(manga.author)}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<h3>
|
<h3>
|
||||||
{'Artist: '}
|
{`${t('manga.label.artist')}: `}
|
||||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<h3>{`Status: ${manga.status}`}</h3>
|
<h3>{`${t('manga.label.status')}: ${manga.status}`}</h3>
|
||||||
<h3>{`Source: ${getSourceName(manga.source)}`}</h3>
|
<h3>{`${t('source.title')}: ${getSourceName(manga.source)}`}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.buttons}>
|
<div className={classes.buttons}>
|
||||||
@@ -174,21 +177,23 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large">
|
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large">
|
||||||
{manga.inLibrary ? <FavoriteIcon sx={{ mr: 1 }} /> : <FavoriteBorderIcon sx={{ mr: 1 }} />}
|
{manga.inLibrary ? <FavoriteIcon sx={{ mr: 1 }} /> : <FavoriteBorderIcon sx={{ mr: 1 }} />}
|
||||||
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
|
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
|
||||||
{manga.inLibrary ? 'In Library' : 'Add To Library'}
|
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<a href={manga.realUrl} target="_blank" rel="noreferrer">
|
<a href={manga.realUrl} target="_blank" rel="noreferrer">
|
||||||
<IconButton size="large">
|
<IconButton size="large">
|
||||||
<PublicIcon sx={{ mr: 1 }} />
|
<PublicIcon sx={{ mr: 1 }} />
|
||||||
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>Open Site</Typography>
|
<Typography sx={{ fontSize: { xs: '0.75em', sm: '0.85em' } }}>
|
||||||
|
{t('global.button.open_site')}
|
||||||
|
</Typography>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.bottom}>
|
<div className={classes.bottom}>
|
||||||
<div className={classes.description}>
|
<div className={classes.description}>
|
||||||
<h4>About</h4>
|
<h4>{t('settings.about.title')}</h4>
|
||||||
<p>{manga.description}</p>
|
<p>{manga.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.genre}>
|
<div className={classes.genre}>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import CategorySelect from 'components/navbar/action/CategorySelect';
|
import CategorySelect from 'components/navbar/action/CategorySelect';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IManga } from 'typings';
|
import { IManga } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
manga: IManga;
|
manga: IManga;
|
||||||
@@ -29,6 +30,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
|
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
<>
|
<>
|
||||||
{isLargeScreen && (
|
{isLargeScreen && (
|
||||||
<>
|
<>
|
||||||
<Tooltip title="Reload data from source">
|
<Tooltip title={t('manga.label.reload_from_source')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onRefresh();
|
onRefresh();
|
||||||
@@ -55,7 +57,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{manga.inLibrary && (
|
{manga.inLibrary && (
|
||||||
<Tooltip title="Edit manga categories">
|
<Tooltip title={t('manga.label.edit_categories')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditCategories(true);
|
setEditCategories(true);
|
||||||
@@ -97,7 +99,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Refresh fontSize="small" />
|
<Refresh fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Reload data from source</ListItemText>
|
<ListItemText>{t('manga.label.reload_from_source')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{manga.inLibrary && (
|
{manga.inLibrary && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -109,7 +111,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Label fontSize="small" />
|
<Label fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Edit manga categories</ListItemText>
|
<ListItemText>{t('manga.label.edit_categories')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { PlayArrow } from '@mui/icons-material';
|
import { PlayArrow } from '@mui/icons-material';
|
||||||
import { BACK } from 'util/useBackTo';
|
import { BACK } from 'util/useBackTo';
|
||||||
import { IChapter } from 'typings';
|
import { IChapter } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface ResumeFABProps {
|
interface ResumeFABProps {
|
||||||
chapter: IChapter;
|
chapter: IChapter;
|
||||||
@@ -18,6 +19,8 @@ interface ResumeFABProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ResumeFab(props: ResumeFABProps) {
|
export default function ResumeFab(props: ResumeFABProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
chapter: { index, lastPageRead },
|
chapter: { index, lastPageRead },
|
||||||
mangaId,
|
mangaId,
|
||||||
@@ -34,7 +37,7 @@ export default function ResumeFab(props: ResumeFABProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlayArrow />
|
<PlayArrow />
|
||||||
{index === 1 ? 'Start' : 'Resume'}
|
{index === 1 ? t('global.button.start') : t('global.button.resume')}
|
||||||
</Fab>
|
</Fab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||||
import { Fab, Menu } from '@mui/material';
|
import { Fab, Menu } from '@mui/material';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { pluralize } from 'components/util/helpers';
|
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
||||||
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem';
|
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export type SelectionAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
export type SelectionAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ interface SelectionFABProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { selectedChapters, onAction } = props;
|
const { selectedChapters, onAction } = props;
|
||||||
const count = selectedChapters.length;
|
const count = selectedChapters.length;
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
|||||||
ref={anchorEl}
|
ref={anchorEl}
|
||||||
>
|
>
|
||||||
<Fab variant="extended" color="primary" id="selectionMenuButton" onClick={() => setOpen(true)}>
|
<Fab variant="extended" color="primary" id="selectionMenuButton" onClick={() => setOpen(true)}>
|
||||||
{`${count} ${pluralize(count, 'chapter')}`}
|
{`${count} ${t('chapter.title', { count })}`}
|
||||||
<MoreHoriz sx={{ ml: 1 }} />
|
<MoreHoriz sx={{ ml: 1 }} />
|
||||||
</Fab>
|
</Fab>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -64,37 +66,37 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
|||||||
({ chapter: c, downloadChapter: dc }) => !c.downloaded && dc === undefined,
|
({ chapter: c, downloadChapter: dc }) => !c.downloaded && dc === undefined,
|
||||||
)}
|
)}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
title="Download selected"
|
title={t('chapter.action.download.add.button.selected')}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem
|
<SelectionFABActionItem
|
||||||
action="delete"
|
action="delete"
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.downloaded)}
|
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.downloaded)}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
title="Delete selected"
|
title={t('chapter.action.download.delete.button.selected')}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem
|
<SelectionFABActionItem
|
||||||
action="bookmark"
|
action="bookmark"
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.bookmarked)}
|
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.bookmarked)}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
title="Bookmark selected"
|
title={t('chapter.action.bookmark.add.button.selected')}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem
|
<SelectionFABActionItem
|
||||||
action="unbookmark"
|
action="unbookmark"
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.bookmarked)}
|
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.bookmarked)}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
title="Remove bookmarks from selected"
|
title={t('chapter.action.bookmark.remove.button.selected')}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem
|
<SelectionFABActionItem
|
||||||
action="mark_as_read"
|
action="mark_as_read"
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.read)}
|
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.read)}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
title="Mark selected as read"
|
title={t('chapter.action.mark_as_read.add.button.selected')}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem
|
<SelectionFABActionItem
|
||||||
action="mark_as_unread"
|
action="mark_as_unread"
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.read)}
|
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.read)}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
title="Mark selected as unread"
|
title={t('chapter.action.mark_as_read.remove.button.selected')}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -5,8 +5,16 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { t } from 'i18next';
|
||||||
import { useReducerLocalStorage } from 'util/useLocalStorage';
|
import { useReducerLocalStorage } from 'util/useLocalStorage';
|
||||||
import { ChapterListOptions, ChapterOptionsReducerAction, ChapterSortMode, IChapter, NullAndUndefined } from 'typings';
|
import {
|
||||||
|
ChapterListOptions,
|
||||||
|
ChapterOptionsReducerAction,
|
||||||
|
ChapterSortMode,
|
||||||
|
IChapter,
|
||||||
|
NullAndUndefined,
|
||||||
|
TranslationKey,
|
||||||
|
} from 'typings';
|
||||||
|
|
||||||
const defaultChapterOptions: ChapterListOptions = {
|
const defaultChapterOptions: ChapterListOptions = {
|
||||||
active: false,
|
active: false,
|
||||||
@@ -35,7 +43,7 @@ function chapterOptionsReducer(state: ChapterListOptions, actions: ChapterOption
|
|||||||
case 'showChapterNumber':
|
case 'showChapterNumber':
|
||||||
return { ...state, showChapterNumber: !state.showChapterNumber };
|
return { ...state, showChapterNumber: !state.showChapterNumber };
|
||||||
default:
|
default:
|
||||||
throw Error('This is not a valid Action');
|
throw Error(t('global.error.label.invalid_action'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +103,9 @@ export const useChapterOptions = (mangaId: string) =>
|
|||||||
defaultChapterOptions,
|
defaultChapterOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SORT_OPTIONS: [ChapterSortMode, string][] = [
|
export const SORT_OPTIONS: [ChapterSortMode, TranslationKey][] = [
|
||||||
['source', 'By Source'],
|
['source', 'global.sort.label.by_source'],
|
||||||
['fetchedAt', 'By Fetch date'],
|
['fetchedAt', 'global.sort.label.by_fetch_date'],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const isFilterActive = (options: ChapterListOptions) => {
|
export const isFilterActive = (options: ChapterListOptions) => {
|
||||||
|
|||||||
@@ -10,13 +10,25 @@ import { CircularProgress } from '@mui/material';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IDownloadChapter } from 'typings';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IDownloadChapter, TranslationKey } from 'typings';
|
||||||
|
|
||||||
interface DownloadStateIndicatorProps {
|
interface DownloadStateIndicatorProps {
|
||||||
download: IDownloadChapter;
|
download: IDownloadChapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => (
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in IDownloadChapter['state']]: TranslationKey } = {
|
||||||
|
Downloading: 'download.state.label.downloading',
|
||||||
|
Error: 'download.state.label.error',
|
||||||
|
Finished: 'download.state.label.finished',
|
||||||
|
Queued: 'download.state.label.queued',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -40,10 +52,11 @@ const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ downloa
|
|||||||
>
|
>
|
||||||
<Typography variant="caption" component="div" color="text.secondary">
|
<Typography variant="caption" component="div" color="text.secondary">
|
||||||
{download.progress !== 0 && `${Math.round(download.progress * 100)}%`}
|
{download.progress !== 0 && `${Math.round(download.progress * 100)}%`}
|
||||||
{download.progress === 0 && download.state}
|
{download.progress === 0 && t(DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP[download.state])}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default DownloadStateIndicator;
|
export default DownloadStateIndicator;
|
||||||
|
|||||||
@@ -31,56 +31,54 @@ import { createPortal } from 'react-dom';
|
|||||||
import useBackTo from 'util/useBackTo';
|
import useBackTo from 'util/useBackTo';
|
||||||
import DesktopSideBar from 'components/navbar/navigation/DesktopSideBar';
|
import DesktopSideBar from 'components/navbar/navigation/DesktopSideBar';
|
||||||
import MobileBottomBar from 'components/navbar/navigation/MobileBottomBar';
|
import MobileBottomBar from 'components/navbar/navigation/MobileBottomBar';
|
||||||
// import { useTranslation } from 'react-i18next';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { NavbarItem } from 'typings';
|
import { NavbarItem } from 'typings';
|
||||||
|
|
||||||
const navbarItems: Array<NavbarItem> = [
|
const navbarItems: Array<NavbarItem> = [
|
||||||
{
|
{
|
||||||
path: '/library',
|
path: '/library',
|
||||||
title: t('DefaultNavBar.navbarItems.Library'),
|
title: 'library.title',
|
||||||
SelectedIconComponent: CollectionsBookmarkIcon,
|
SelectedIconComponent: CollectionsBookmarkIcon,
|
||||||
IconComponent: CollectionsOutlinedBookmarkIcon,
|
IconComponent: CollectionsOutlinedBookmarkIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/updates',
|
path: '/updates',
|
||||||
title: 'Updates',
|
title: 'updates.title',
|
||||||
SelectedIconComponent: NewReleasesIcon,
|
SelectedIconComponent: NewReleasesIcon,
|
||||||
IconComponent: NewReleasesOutlinedIcon,
|
IconComponent: NewReleasesOutlinedIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/extensions',
|
path: '/extensions',
|
||||||
title: 'Extensions',
|
title: 'extension.title',
|
||||||
SelectedIconComponent: ExtensionIcon,
|
SelectedIconComponent: ExtensionIcon,
|
||||||
IconComponent: ExtensionOutlinedIcon,
|
IconComponent: ExtensionOutlinedIcon,
|
||||||
show: 'desktop',
|
show: 'desktop',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/sources',
|
path: '/sources',
|
||||||
title: 'Sources',
|
title: 'source.title',
|
||||||
SelectedIconComponent: ExploreIcon,
|
SelectedIconComponent: ExploreIcon,
|
||||||
IconComponent: ExploreOutlinedIcon,
|
IconComponent: ExploreOutlinedIcon,
|
||||||
show: 'desktop',
|
show: 'desktop',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/browse',
|
path: '/browse',
|
||||||
title: 'Browse',
|
title: 'global.label.browse',
|
||||||
SelectedIconComponent: ExploreIcon,
|
SelectedIconComponent: ExploreIcon,
|
||||||
IconComponent: ExploreOutlinedIcon,
|
IconComponent: ExploreOutlinedIcon,
|
||||||
show: 'mobile',
|
show: 'mobile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/downloads',
|
path: '/downloads',
|
||||||
title: 'Downloads',
|
title: 'download.title',
|
||||||
SelectedIconComponent: GetAppIcon,
|
SelectedIconComponent: GetAppIcon,
|
||||||
IconComponent: GetAppOutlinedIcon,
|
IconComponent: GetAppOutlinedIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
title: 'Settings',
|
title: 'settings.title',
|
||||||
SelectedIconComponent: SettingsIcon,
|
SelectedIconComponent: SettingsIcon,
|
||||||
IconComponent: SettingsIcon,
|
IconComponent: SettingsIcon,
|
||||||
show: 'both',
|
show: 'both',
|
||||||
@@ -88,7 +86,6 @@ const navbarItems: Array<NavbarItem> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function DefaultNavBar() {
|
export default function DefaultNavBar() {
|
||||||
// const { t } = useTranslation();
|
|
||||||
const { title, action, override } = useContext(NavBarContext);
|
const { title, action, override } = useContext(NavBarContext);
|
||||||
const backTo = useBackTo();
|
const backTo = useBackTo();
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { styled } from '@mui/system';
|
|||||||
import useBackTo from 'util/useBackTo';
|
import useBackTo from 'util/useBackTo';
|
||||||
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
|
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
|
||||||
import { IChapter, IManga, IMangaCard, IReaderSettings } from 'typings';
|
import { IChapter, IManga, IMangaCard, IReaderSettings } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const Root = styled('div')(({ theme }) => ({
|
const Root = styled('div')(({ theme }) => ({
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -121,6 +122,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ReaderNavBar(props: IProps) {
|
export default function ReaderNavBar(props: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const backTo = useBackTo();
|
const backTo = useBackTo();
|
||||||
const location = useLocation<{
|
const location = useLocation<{
|
||||||
@@ -230,7 +232,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText primary="Reader Settings" />
|
<ListItemText primary={t('reader.settings.title.reader_settings')} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
@@ -258,7 +260,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
<Divider sx={{ my: 1, mx: 2 }} />
|
<Divider sx={{ my: 1, mx: 2 }} />
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<PageNavigation>
|
<PageNavigation>
|
||||||
<span>Currently on page</span>
|
<span>{t('reader.page_info.label.currently_on_page')}</span>
|
||||||
<FormControl size="small" sx={{ margin: '0 5px' }} disabled={chapter.pageCount === -1}>
|
<FormControl size="small" sx={{ margin: '0 5px' }} disabled={chapter.pageCount === -1}>
|
||||||
<Select
|
<Select
|
||||||
MenuProps={MenuProps}
|
MenuProps={MenuProps}
|
||||||
@@ -278,11 +280,11 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<span>{`of ${chapter.pageCount}`}</span>
|
<span>{t('reader.page_info.label.of_max_pages', { maxPages: chapter.pageCount })}</span>
|
||||||
</PageNavigation>
|
</PageNavigation>
|
||||||
<ChapterNavigation>
|
<ChapterNavigation>
|
||||||
<IconButton
|
<IconButton
|
||||||
title="Previous Chapter"
|
title={t('reader.button.previous_chapter')}
|
||||||
sx={{ gridArea: 'pre' }}
|
sx={{ gridArea: 'pre' }}
|
||||||
disabled={chapter.index <= 1}
|
disabled={chapter.index <= 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -317,14 +319,14 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
.map((ignoreValue, index) => (
|
.map((ignoreValue, index) => (
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`Chapter ${
|
<MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`${t(
|
||||||
index + 1
|
'chapter.title',
|
||||||
}`}</MenuItem>
|
)} ${index + 1}`}</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<IconButton
|
<IconButton
|
||||||
title="Next Chapter"
|
title={t('reader.button.next_chapter')}
|
||||||
sx={{ gridArea: 'next' }}
|
sx={{ gridArea: 'next' }}
|
||||||
disabled={chapter.index < 1 || chapter.index >= chapter.chapterCount}
|
disabled={chapter.index < 1 || chapter.index >= chapter.chapterCount}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
|||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import { ICategory } from 'typings';
|
import { ICategory } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -24,6 +25,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CategorySelect(props: IProps) {
|
export default function CategorySelect(props: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { open, setOpen, mangaId } = props;
|
const { open, setOpen, mangaId } = props;
|
||||||
|
|
||||||
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(`/api/v1/manga/${mangaId}/category`);
|
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(`/api/v1/manga/${mangaId}/category`);
|
||||||
@@ -65,14 +68,14 @@ export default function CategorySelect(props: IProps) {
|
|||||||
maxWidth="xs"
|
maxWidth="xs"
|
||||||
open={open}
|
open={open}
|
||||||
>
|
>
|
||||||
<DialogTitle>Set categories</DialogTitle>
|
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{allCategories.length === 0 && (
|
{allCategories.length === 0 && (
|
||||||
<span>
|
<span>
|
||||||
No categories found!
|
{t('category.error.no_categories_found.label.info')}
|
||||||
<br />
|
<br />
|
||||||
You should make some from settings.
|
{t('category.error.no_categories_found.label.hint')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{allCategories.map((category) => (
|
{allCategories.map((category) => (
|
||||||
@@ -92,10 +95,10 @@ export default function CategorySelect(props: IProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="primary">
|
<Button onClick={handleOk} color="primary">
|
||||||
Ok
|
{t('global.button.ok')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { List, ListItemSecondaryAction, ListItemText } from '@mui/material';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import { langCodeToName } from 'util/language';
|
import { langCodeToName } from 'util/language';
|
||||||
import cloneObject from 'util/cloneObject';
|
import cloneObject from 'util/cloneObject';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function removeAll(firstList: any[], secondList: any[]) {
|
function removeAll(firstList: any[], secondList: any[]) {
|
||||||
secondList.forEach((item) => {
|
secondList.forEach((item) => {
|
||||||
@@ -38,6 +39,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function LangSelect(props: IProps) {
|
export default function LangSelect(props: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { shownLangs, setShownLangs, allLangs, forcedLangs } = props;
|
const { shownLangs, setShownLangs, allLangs, forcedLangs } = props;
|
||||||
// hold a copy and only sate state on parent when OK pressed, improves performance
|
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||||
const [mShownLangs, setMShownLangs] = useState(removeAll(cloneObject(shownLangs), forcedLangs!));
|
const [mShownLangs, setMShownLangs] = useState(removeAll(cloneObject(shownLangs), forcedLangs!));
|
||||||
@@ -85,7 +88,7 @@ export default function LangSelect(props: IProps) {
|
|||||||
maxWidth="xs"
|
maxWidth="xs"
|
||||||
open={open}
|
open={open}
|
||||||
>
|
>
|
||||||
<DialogTitle>Enabled Languages</DialogTitle>
|
<DialogTitle>{t('global.language.title.enabled_languages')}</DialogTitle>
|
||||||
<DialogContent dividers sx={{ padding: 0 }}>
|
<DialogContent dividers sx={{ padding: 0 }}>
|
||||||
<List>
|
<List>
|
||||||
{allLangs.map((lang) => (
|
{allLangs.map((lang) => (
|
||||||
@@ -104,10 +107,10 @@ export default function LangSelect(props: IProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="primary">
|
<Button onClick={handleOk} color="primary">
|
||||||
Ok
|
{t('global.button.ok')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Link, useLocation } from 'react-router-dom';
|
|||||||
import { styled } from '@mui/system';
|
import { styled } from '@mui/system';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { NavbarItem } from 'typings';
|
import { NavbarItem } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const SideNavBarContainer = styled('div')(({ theme }) => ({
|
const SideNavBarContainer = styled('div')(({ theme }) => ({
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
@@ -29,6 +30,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DesktopSideBar({ navBarItems }: IProps) {
|
export default function DesktopSideBar({ navBarItems }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ export default function DesktopSideBar({ navBarItems }: IProps) {
|
|||||||
<Link to={path} style={{ color: 'inherit', textDecoration: 'none' }} key={path}>
|
<Link to={path} style={{ color: 'inherit', textDecoration: 'none' }} key={path}>
|
||||||
<ListItem disableRipple button key={title}>
|
<ListItem disableRipple button key={title}>
|
||||||
<ListItemIcon sx={{ minWidth: '0' }}>
|
<ListItemIcon sx={{ minWidth: '0' }}>
|
||||||
<Tooltip placement="right" title={title}>
|
<Tooltip placement="right" title={t(title)}>
|
||||||
{iconFor(path, IconComponent, SelectedIconComponent)}
|
{iconFor(path, IconComponent, SelectedIconComponent)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { styled, Box } from '@mui/system';
|
|||||||
import { Link as RRDLink, useLocation } from 'react-router-dom';
|
import { Link as RRDLink, useLocation } from 'react-router-dom';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { NavbarItem } from 'typings';
|
import { NavbarItem } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const BottomNavContainer = styled('div')(({ theme }) => ({
|
const BottomNavContainer = styled('div')(({ theme }) => ({
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@@ -38,6 +39,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MobileBottomBar({ navBarItems }: IProps) {
|
export default function MobileBottomBar({ navBarItems }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ export default function MobileBottomBar({ navBarItems }: IProps) {
|
|||||||
: 'grey.600',
|
: 'grey.600',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{t(title)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Select from '@mui/material/Select';
|
|||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IReaderSettings } from 'typings';
|
import { IReaderSettings } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IProps extends IReaderSettings {
|
interface IProps extends IReaderSettings {
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
||||||
@@ -24,10 +25,12 @@ export default function ReaderSettingsOptions({
|
|||||||
showPageNumber,
|
showPageNumber,
|
||||||
setSettingValue,
|
setSettingValue,
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Static Navigation" />
|
<ListItemText primary={t('reader.settings.label.static_navigation')} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -37,7 +40,7 @@ export default function ReaderSettingsOptions({
|
|||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Show page number" />
|
<ListItemText primary={t('reader.settings.label.show_page_number')} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -47,7 +50,7 @@ export default function ReaderSettingsOptions({
|
|||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Load next chapter at ending" />
|
<ListItemText primary={t('reader.settings.label.load_next_chapter')} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
@@ -57,24 +60,30 @@ export default function ReaderSettingsOptions({
|
|||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Reader Type" />
|
<ListItemText primary={t('reader.settings.label.reader_type')} />
|
||||||
<Select
|
<Select
|
||||||
variant="standard"
|
variant="standard"
|
||||||
value={readerType}
|
value={readerType}
|
||||||
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
||||||
sx={{ p: 0 }}
|
sx={{ p: 0 }}
|
||||||
>
|
>
|
||||||
<MenuItem value="SingleLTR">Single Page (LTR)</MenuItem>
|
<MenuItem value="SingleLTR">{t('reader.settings.reader_type.label.single_page_ltr')}</MenuItem>
|
||||||
<MenuItem value="SingleRTL">Single Page (RTL)</MenuItem>
|
<MenuItem value="SingleRTL">{t('reader.settings.reader_type.label.single_page_rtl')}</MenuItem>
|
||||||
{/* <MenuItem value="SingleVertical">
|
{/* <MenuItem value="SingleVertical">
|
||||||
Vertical(WIP)
|
Vertical(WIP)
|
||||||
</MenuItem> */}
|
</MenuItem> */}
|
||||||
<MenuItem value="DoubleLTR">Double Page (LTR)</MenuItem>
|
<MenuItem value="DoubleLTR">{t('reader.settings.reader_type.label.double_page_ltr')}</MenuItem>
|
||||||
<MenuItem value="DoubleRTL">Double Page (RTL)</MenuItem>
|
<MenuItem value="DoubleRTL">{t('reader.settings.reader_type.label.double_page_rtl')}</MenuItem>
|
||||||
<MenuItem value="Webtoon">Webtoon</MenuItem>
|
<MenuItem value="Webtoon">{t('reader.settings.reader_type.label.webtoon')}</MenuItem>
|
||||||
<MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
|
<MenuItem value="ContinuesVertical">
|
||||||
<MenuItem value="ContinuesHorizontalLTR">Horizontal (LTR)</MenuItem>
|
{t('reader.settings.reader_type.label.continuous_vertical')}
|
||||||
<MenuItem value="ContinuesHorizontalRTL">Horizontal (RTL)</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem value="ContinuesHorizontalLTR">
|
||||||
|
{t('reader.settings.reader_type.label.continuous_horizontal_ltr')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="ContinuesHorizontalRTL">
|
||||||
|
{t('reader.settings.reader_type.label.continuous_horizontal_rtl')}
|
||||||
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import { IconButton, Menu, MenuItem, FormControlLabel, Radio } from '@mui/materi
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||||
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
||||||
export default function SourceGridLayout() {
|
export default function SourceGridLayout() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: { SourcegridLayout },
|
options: { SourcegridLayout },
|
||||||
setOptions,
|
setOptions,
|
||||||
@@ -53,7 +56,7 @@ export default function SourceGridLayout() {
|
|||||||
>
|
>
|
||||||
<MenuItem onClick={handleClose}>
|
<MenuItem onClick={handleClose}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="Compact grid"
|
label={t('global.grid_layout.label.compact_grid')}
|
||||||
value={GridLayout.Compact}
|
value={GridLayout.Compact}
|
||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
@@ -66,7 +69,7 @@ export default function SourceGridLayout() {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleClose}>
|
<MenuItem onClick={handleClose}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="Comfortable grid"
|
label={t('global.grid_layout.label.comfortable_grid')}
|
||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.Comfortable.toString()}
|
name={GridLayout.Comfortable.toString()}
|
||||||
@@ -78,7 +81,7 @@ export default function SourceGridLayout() {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleClose}>
|
<MenuItem onClick={handleClose}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="List"
|
label={t('global.grid_layout.label.list')}
|
||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.List.toString()}
|
name={GridLayout.List.toString()}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MangaGrid, { IMangaGridProps } from 'components/MangaGrid';
|
import MangaGrid, { IMangaGridProps } from 'components/MangaGrid';
|
||||||
import { IMangaCard } from 'typings';
|
import { IMangaCard } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
const FILTERED_OUT_MESSAGE = 'There are no Manga matching this filter';
|
|
||||||
|
|
||||||
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
||||||
return mangas;
|
return mangas;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SourceMangaGrid(props: IMangaGridProps) {
|
export default function SourceMangaGrid(props: IMangaGridProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message, messageExtra, gridLayout } = props;
|
const { mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message, messageExtra, gridLayout } = props;
|
||||||
|
|
||||||
const filteredManga = filterManga(mangas);
|
const filteredManga = filterManga(mangas);
|
||||||
@@ -29,7 +29,7 @@ export default function SourceMangaGrid(props: IMangaGridProps) {
|
|||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
lastPageNum={lastPageNum}
|
lastPageNum={lastPageNum}
|
||||||
setLastPageNum={setLastPageNum}
|
setLastPageNum={setLastPageNum}
|
||||||
message={showFilteredOutMessage ? FILTERED_OUT_MESSAGE : message}
|
message={showFilteredOutMessage ? t('manga.error.label.no_matches') : message}
|
||||||
messageExtra={messageExtra}
|
messageExtra={messageExtra}
|
||||||
gridLayout={gridLayout}
|
gridLayout={gridLayout}
|
||||||
inLibraryIndicator
|
inLibraryIndicator
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import TriStateFilter from 'components/source/filters/TriStateFilter';
|
|||||||
import GroupFilter from 'components/source/filters/GroupFilter';
|
import GroupFilter from 'components/source/filters/GroupFilter';
|
||||||
import SeperatorFilter from 'components/source/filters/SeparatorFilter';
|
import SeperatorFilter from 'components/source/filters/SeparatorFilter';
|
||||||
import { ISourceFilters, IState } from 'typings';
|
import { ISourceFilters, IState } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IFilters {
|
interface IFilters {
|
||||||
sourceFilter: ISourceFilters[];
|
sourceFilter: ISourceFilters[];
|
||||||
@@ -143,6 +144,7 @@ export default function SourceOptions({
|
|||||||
setSearch,
|
setSearch,
|
||||||
update,
|
update,
|
||||||
}: IFilters1) {
|
}: IFilters1) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [FilterOptions, setFilterOptions] = React.useState(false);
|
const [FilterOptions, setFilterOptions] = React.useState(false);
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
@@ -165,14 +167,14 @@ export default function SourceOptions({
|
|||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<FilterListIcon />
|
<FilterListIcon />
|
||||||
Filter
|
{t('global.button.filter')}
|
||||||
</Fab>
|
</Fab>
|
||||||
|
|
||||||
<OptionsPanel open={FilterOptions} onClose={() => setFilterOptions(false)}>
|
<OptionsPanel open={FilterOptions} onClose={() => setFilterOptions(false)}>
|
||||||
<Box sx={{ display: 'flex', p: 2, pb: 0 }}>
|
<Box sx={{ display: 'flex', p: 2, pb: 0 }}>
|
||||||
<Button onClick={handleReset}>Reset</Button>
|
<Button onClick={handleReset}>{t('global.button.reset')}</Button>
|
||||||
<Button sx={{ marginLeft: 'auto' }} variant="contained" onClick={handleSubmit}>
|
<Button sx={{ marginLeft: 'auto' }} variant="contained" onClick={handleSubmit}>
|
||||||
Submit
|
{t('global.button.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import DialogActions from '@mui/material/DialogActions';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { EditTextPreferenceProps } from 'typings';
|
import { EditTextPreferenceProps } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function EditTextPreference(props: EditTextPreferenceProps) {
|
export default function EditTextPreference(props: EditTextPreferenceProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props;
|
const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props;
|
||||||
|
|
||||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
||||||
@@ -57,10 +60,10 @@ export default function EditTextPreference(props: EditTextPreferenceProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleDialogCancel} color="primary">
|
<Button onClick={handleDialogCancel} color="primary">
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDialogSubmit} color="primary">
|
<Button onClick={handleDialogSubmit} color="primary">
|
||||||
OK
|
{t('global.button.ok')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import Radio from '@mui/material/Radio';
|
|||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { ListPreferenceProps } from 'typings';
|
import { ListPreferenceProps } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IListDialogProps {
|
interface IListDialogProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -27,6 +28,8 @@ interface IListDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListDialog(props: IListDialogProps) {
|
function ListDialog(props: IListDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { value: valueProp, open, onClose, options, title } = props;
|
const { value: valueProp, open, onClose, options, title } = props;
|
||||||
const [value, setValue] = React.useState(valueProp);
|
const [value, setValue] = React.useState(valueProp);
|
||||||
const radioGroupRef = React.useRef<HTMLDivElement>(null);
|
const radioGroupRef = React.useRef<HTMLDivElement>(null);
|
||||||
@@ -72,9 +75,9 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel}>
|
<Button autoFocus onClick={handleCancel}>
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk}>Ok</Button>
|
<Button onClick={handleOk}>{t('global.button.ok')}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import cloneObject from 'util/cloneObject';
|
import cloneObject from 'util/cloneObject';
|
||||||
import { MultiSelectListPreferenceProps } from 'typings';
|
import { MultiSelectListPreferenceProps } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IListDialogProps {
|
interface IListDialogProps {
|
||||||
selectedValues: string[];
|
selectedValues: string[];
|
||||||
@@ -28,6 +29,8 @@ interface IListDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListDialog(props: IListDialogProps) {
|
function ListDialog(props: IListDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { selectedValues: selectedValuesProp, open, onClose, values, title } = props;
|
const { selectedValues: selectedValuesProp, open, onClose, values, title } = props;
|
||||||
const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp);
|
const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp);
|
||||||
|
|
||||||
@@ -86,9 +89,9 @@ function ListDialog(props: IListDialogProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel}>
|
<Button autoFocus onClick={handleCancel}>
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk}>Ok</Button>
|
<Button onClick={handleOk}>{t('global.button.ok')}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const pluralize = (count: number, input: string | { one: string; many: string }) => {
|
|
||||||
if (typeof input === 'string') {
|
|
||||||
return `${input}${count === 1 ? '' : 's'}`;
|
|
||||||
}
|
|
||||||
return input[count === 1 ? 'one' : 'many'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const interpolate = (count: number, input: { one: string; many: string }) => {
|
|
||||||
const text = count === 1 ? input.one : input.many;
|
|
||||||
return text.replaceAll('%count%', count.toString());
|
|
||||||
};
|
|
||||||
9
src/i18n/i18next.d.ts
vendored
Normal file
9
src/i18n/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import 'i18next';
|
||||||
|
import resources from 'i18n/translations';
|
||||||
|
|
||||||
|
declare module 'i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
returnNull: false;
|
||||||
|
resources: typeof resources.en;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,426 @@
|
|||||||
{
|
{
|
||||||
"screens": {
|
"category": {
|
||||||
"Library": {
|
"dialog": {
|
||||||
"Library": "Library",
|
"title": {
|
||||||
"could-not-load-categories": "Could not load categories",
|
"edit_category": "Edit Catalog",
|
||||||
"your-library-is-empty": "Your Library is empty",
|
"new_category": "New Catalog"
|
||||||
"category-is-empty": "Category is Empty",
|
}
|
||||||
"could-not-load-manga": "Could not load manga"
|
|
||||||
},
|
},
|
||||||
"Browse": {
|
"error": {
|
||||||
"sources": "Sources",
|
"label": {
|
||||||
"extensions": "Extensions"
|
"empty": "Category is Empty",
|
||||||
|
"request_failure": "Could not load categories"
|
||||||
},
|
},
|
||||||
"DownloadQueue": {
|
"no_categories_found": {
|
||||||
"no-downloads": "No downloads",
|
"label": {
|
||||||
"download-queue": "Download Queue"
|
"hint": "You should make some from settings.",
|
||||||
|
"info": "No categories found!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"category_name": "Category Name",
|
||||||
|
"use_as_default_category": "Default category when adding new manga to library"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"categories": "Categories",
|
||||||
|
"set_categories": "Set categories"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chapter": {
|
||||||
|
"action": {
|
||||||
|
"bookmark": {
|
||||||
|
"add": {
|
||||||
|
"button": {
|
||||||
|
"selected": "Bookmark selected"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"action": "Add bookmark",
|
||||||
|
"error_one": "Error bookmarking chapter",
|
||||||
|
"error_other": "Error bookmarking chapters",
|
||||||
|
"success_one": "Chapter bookmarked",
|
||||||
|
"success_other": "{{count}} chapters bookmarked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"button": {
|
||||||
|
"selected": "Remove bookmarks from selected"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"action": "Remove bookmark",
|
||||||
|
"error_one": "Error removing bookmark",
|
||||||
|
"error_other": "Error removing bookmarks",
|
||||||
|
"success_one": "Chapter bookmark removed",
|
||||||
|
"success_other": "{{count}} chapter bookmarks removed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"add": {
|
||||||
|
"button": {
|
||||||
|
"selected": "Download selected"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"action": "Download",
|
||||||
|
"error_one": "Error adding download",
|
||||||
|
"error_other": "Error adding downloads",
|
||||||
|
"success_one": "Download added",
|
||||||
|
"success_other": "{{count}} downloads added"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"button": {
|
||||||
|
"selected": "Delete selected"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"action": "Delete",
|
||||||
|
"error_one": "Error deleting chapter",
|
||||||
|
"error_other": "Error deleting chapters",
|
||||||
|
"success_one": "Chapter deleted",
|
||||||
|
"success_other": "{{count}} chapters deleted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"select": "Select"
|
||||||
|
},
|
||||||
|
"mark_as_read": {
|
||||||
|
"add": {
|
||||||
|
"button": {
|
||||||
|
"selected": "Mark selected as read"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"action": {
|
||||||
|
"current": "Mark as read",
|
||||||
|
"previous": "Mark previous as Read"
|
||||||
|
},
|
||||||
|
"error_one": "Error marking chapter as read",
|
||||||
|
"error_other": "Error marking chapters as read",
|
||||||
|
"success_one": "Chapter marked as read",
|
||||||
|
"success_other": "{{count}} chapters marked as read"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"button": {
|
||||||
|
"selected": "Mark selected as unread"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"action": "Mark as unread",
|
||||||
|
"error_one": "Error marking chapter as unread",
|
||||||
|
"error_other": "Error marking chapters as unread",
|
||||||
|
"success_one": "Chapter marked as unread",
|
||||||
|
"success_other": "{{count}} chapters marked as unread"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"no_chapter_found": "No chapters found",
|
||||||
|
"no_matches": "No chapters matching filter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"option": {
|
||||||
|
"display": {
|
||||||
|
"label": {
|
||||||
|
"chapter_number": "Chapter Number",
|
||||||
|
"source_title": "Source Title"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"label": {
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Chapter",
|
||||||
|
"title_one": "Chapter",
|
||||||
|
"title_other": "Chapters"
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"queue": {
|
||||||
|
"label": {
|
||||||
|
"no_downloads": "No downloads"
|
||||||
|
},
|
||||||
|
"title": "Download Queue"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"label": {
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"error": "Error",
|
||||||
|
"finished": "Finished",
|
||||||
|
"queued": "Queued"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Downloads"
|
||||||
|
},
|
||||||
|
"extension": {
|
||||||
|
"action": {
|
||||||
|
"label": {
|
||||||
|
"install": "install",
|
||||||
|
"uninstall": "uninstall",
|
||||||
|
"update": "update"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"installation_failed": "Extension installation failed!",
|
||||||
|
"installed_successfully": "Installed extension successfully!",
|
||||||
|
"installing_file": "Installing Extension File..."
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"all": "All"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"label": {
|
||||||
|
"installing": "installing",
|
||||||
|
"obsolete": "obsolete",
|
||||||
|
"uninstalling": "uninstalling",
|
||||||
|
"updating": "updating"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Extensions"
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"button": {
|
||||||
|
"browse": "Browse",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"clear": "Clear",
|
||||||
|
"filter": "Filter",
|
||||||
|
"latest": "Latest",
|
||||||
|
"ok": "Ok",
|
||||||
|
"open_site": "Open Site",
|
||||||
|
"reset": "Reset",
|
||||||
|
"reset_to_default": "Reset to Default",
|
||||||
|
"resume": "Resume",
|
||||||
|
"select_all": "Select all",
|
||||||
|
"set": "Set",
|
||||||
|
"start": "Start",
|
||||||
|
"submit": "Submit"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"label": {
|
||||||
|
"today": "TODAY",
|
||||||
|
"today_at": "Today at {{timeString}}",
|
||||||
|
"yesterday": "YESTERDAY",
|
||||||
|
"yesterday_at": "Yesterday at {{timeString}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"invalid_action": "This is not a valid Action",
|
||||||
|
"invalid_file_type": "invalid file type!",
|
||||||
|
"update_failed": "Checking for updates failed!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"label": {
|
||||||
|
"bookmarked": "Bookmarked",
|
||||||
|
"downloaded": "Downloaded",
|
||||||
|
"unread": "Unread"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grid_layout": {
|
||||||
|
"label": {
|
||||||
|
"comfortable_grid": "Comfortable grid",
|
||||||
|
"compact_grid": "Compact grid",
|
||||||
|
"list": "List"
|
||||||
|
},
|
||||||
|
"title": "Display mode"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"browse": "Browse",
|
||||||
|
"display": "Display",
|
||||||
|
"filter": "Filter",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"sort": "Sort"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"label": {
|
||||||
|
"language_with_code": "language with code: {{code}}"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"enabled_languages": "Enabled Languages"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"label": {
|
||||||
|
"by_fetch_date": "By Fetch date",
|
||||||
|
"by_source": "By Source"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"empty": "Your Library is empty",
|
||||||
|
"no_matches": "There are no Manga matching this filter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"option": {
|
||||||
|
"display": {
|
||||||
|
"badge": {
|
||||||
|
"label": {
|
||||||
|
"download_badges": "Download Badges",
|
||||||
|
"unread_badges": "Unread Badges"
|
||||||
|
},
|
||||||
|
"title": "Badges"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"label": {
|
||||||
|
"alphabetically": "Alphabetically",
|
||||||
|
"by_date_added": "By Date Added",
|
||||||
|
"by_last_read": "By Last Read",
|
||||||
|
"by_unread_chapters": "By Unread chapters"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Library"
|
||||||
|
},
|
||||||
|
"manga": {
|
||||||
|
"button": {
|
||||||
|
"add_to_library": "Add To Library",
|
||||||
|
"in_library": "In Library"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"no_mangas_found": "No manga found!",
|
||||||
|
"no_matches": "There are no Manga matching this filter",
|
||||||
|
"request_failure": "Could not load manga"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"artist": "Artist",
|
||||||
|
"author": "Author",
|
||||||
|
"edit_categories": "Edit manga categories",
|
||||||
|
"reload_from_source": "Reload data from source",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"title": "Manga"
|
||||||
|
},
|
||||||
|
"reader": {
|
||||||
|
"button": {
|
||||||
|
"next_chapter": "Next Chapter",
|
||||||
|
"previous_chapter": "Previous Chapter"
|
||||||
|
},
|
||||||
|
"page_info": {
|
||||||
|
"label": {
|
||||||
|
"currently_on_page": "Currently on page",
|
||||||
|
"of_max_pages": "of {{maxPages}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"failed_to_save_default_settings": "Failed to save the default reader settings to the server",
|
||||||
|
"failed_to_save_settings": "Failed to save the reader settings to the server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"load_next_chapter": "Load next chapter at ending",
|
||||||
|
"reader_type": "Reader Type",
|
||||||
|
"show_page_number": "Show page number",
|
||||||
|
"static_navigation": "Static Navigation"
|
||||||
|
},
|
||||||
|
"reader_type": {
|
||||||
|
"label": {
|
||||||
|
"continuous_horizontal_ltr": "Horizontal (LTR)",
|
||||||
|
"continuous_horizontal_rtl": "Horizontal (RTL)",
|
||||||
|
"continuous_vertical": "Continues Vertical",
|
||||||
|
"double_page_ltr": "Double Page (LTR)",
|
||||||
|
"double_page_rtl": "Double Page (RTL)",
|
||||||
|
"single_page_ltr": "Single Page (LTR)",
|
||||||
|
"single_page_rtl": "Single Page (RTL)",
|
||||||
|
"webtoon": "Webtoon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"default_reader_settings": "Default Reader Settings",
|
||||||
|
"reader_settings": "Reader Settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Reader - Manga {{mangaId}} Chapter {{chapterIndex}}"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"failed_to_save_settings": "Failed to save the default search settings to the server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"ignore_filters": "Ignore Filters when Searching"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"global_search": "Global Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"about": {
|
||||||
|
"label": {
|
||||||
|
"build_time": "Build time",
|
||||||
|
"discord": "Discord",
|
||||||
|
"github": "Github",
|
||||||
|
"server": "Server",
|
||||||
|
"server_address": "Server Address",
|
||||||
|
"server_version": "Server version"
|
||||||
|
},
|
||||||
|
"title": "About"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"label": {
|
||||||
|
"backup_restore_failed": "Backup restore failed!",
|
||||||
|
"create_backup": "Create Backup",
|
||||||
|
"create_backup_info": "Backup library as a Tachiyomi backup",
|
||||||
|
"legacy_backup_unsupported": "legacy backups are not supported!",
|
||||||
|
"restore_backup": "Restore Backup",
|
||||||
|
"restore_backup_info": "You can also drag and drop the backup file here to restore",
|
||||||
|
"restored_backup": "Backup restore finished!",
|
||||||
|
"restoring_backup": "Restoring backup..."
|
||||||
|
},
|
||||||
|
"title": "Backup"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"dark_theme": "Dark Theme",
|
||||||
|
"image_cache": "Use image cache",
|
||||||
|
"image_cache_description": "Disabling image cache makes images load faster if you have a slow disk, but uses it much more internet traffic in turn",
|
||||||
|
"manga_item_width": "Manga Item width",
|
||||||
|
"show_nsfw": "Show NSFW",
|
||||||
|
"show_nsfw_description": "Hide NSFW extensions and sources"
|
||||||
|
},
|
||||||
|
"server_address": {
|
||||||
|
"dialog": {
|
||||||
|
"label": {
|
||||||
|
"enter_address": "Enter Server Address"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"configuration": {
|
||||||
|
"title": "Source Configuration"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"no_sources_found": "No sources found. Install Some Extensions first."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"local_source": {
|
||||||
|
"label": {
|
||||||
|
"checkout": "Check out",
|
||||||
|
"guide": "Local source guide"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Source",
|
||||||
|
"title_one": "Source",
|
||||||
|
"title_other": "Sources"
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"no_updates_available": "You don't have any updates yet."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Updates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"screens": {
|
"library": {
|
||||||
"Library": {
|
"title": "المكتبة"
|
||||||
"Library": "المكتبة"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
{
|
{
|
||||||
"screens": {
|
"category": {
|
||||||
"Library": {
|
"error": {
|
||||||
"Library": "Bibliotek",
|
"label": {
|
||||||
"could-not-load-categories": "Kategorien konnten nicht geladen werden",
|
"empty": "Kategorie ist leer",
|
||||||
"category-is-empty": "Kategorie ist leer",
|
"request_failure": "Kategorien konnten nicht geladen werden"
|
||||||
"could-not-load-manga": "Manga konnte nicht geladen werden",
|
}
|
||||||
"your-library-is-empty": "Deine Bibliothek ist leer"
|
}
|
||||||
},
|
},
|
||||||
"Browse": {
|
"download": {
|
||||||
"sources": "Quellen",
|
"queue": {
|
||||||
"extensions": "Erweiterungen"
|
"label": {
|
||||||
|
"no_downloads": "Keine Downloads"
|
||||||
},
|
},
|
||||||
"DownloadQueue": {
|
"title": "Herunterladen-Warteschlange"
|
||||||
"no-downloads": "Keine Downloads",
|
}
|
||||||
"download-queue": "Herunterladen-Warteschlange"
|
},
|
||||||
|
"extension": {
|
||||||
|
"title": "Erweiterungen"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"empty": "Deine Bibliothek ist leer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Bibliotek"
|
||||||
|
},
|
||||||
|
"manga": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"request_failure": "Manga konnte nicht geladen werden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"title": "Quellen"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
{
|
{
|
||||||
"screens": {
|
"category": {
|
||||||
"Library": {
|
"error": {
|
||||||
"Library": "Biblioteca",
|
"label": {
|
||||||
"could-not-load-categories": "No se pudieron cargar las categorías",
|
"empty": "La categoría está vacía",
|
||||||
"your-library-is-empty": "Tu biblioteca esta vacia",
|
"request_failure": "No se pudieron cargar las categorías"
|
||||||
"category-is-empty": "La categoría está vacía",
|
}
|
||||||
"could-not-load-manga": "No se pudo cargar el manga"
|
}
|
||||||
},
|
},
|
||||||
"DownloadQueue": {
|
"download": {
|
||||||
"no-downloads": "No hay descargas",
|
"queue": {
|
||||||
"download-queue": "Descarga en la cola"
|
"label": {
|
||||||
|
"no_downloads": "No hay descargas"
|
||||||
},
|
},
|
||||||
"Browse": {
|
"title": "Descarga en la cola"
|
||||||
"extensions": "Extensiones",
|
}
|
||||||
"sources": "Fuentes"
|
},
|
||||||
|
"extension": {
|
||||||
|
"title": "Extensiones"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"empty": "Tu biblioteca esta vacia"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Biblioteca"
|
||||||
|
},
|
||||||
|
"manga": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"request_failure": "No se pudo cargar el manga"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"title": "Fuentes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
{
|
{
|
||||||
"screens": {
|
"category": {
|
||||||
"Library": {
|
"error": {
|
||||||
"Library": "Bibliothèque",
|
"label": {
|
||||||
"could-not-load-categories": "Impossible de charger les catégories",
|
"empty": "La catégorie est vide",
|
||||||
"your-library-is-empty": "Votre bibliothèque est vide",
|
"request_failure": "Impossible de charger les catégories"
|
||||||
"category-is-empty": "La catégorie est vide",
|
}
|
||||||
"could-not-load-manga": "Impossible de charger le manga"
|
}
|
||||||
},
|
},
|
||||||
"Browse": {
|
"download": {
|
||||||
"sources": "Sources",
|
"queue": {
|
||||||
"extensions": "Extensions"
|
"label": {
|
||||||
|
"no_downloads": "Aucun téléchargement"
|
||||||
},
|
},
|
||||||
"DownloadQueue": {
|
"title": "File d’attente des téléchargements"
|
||||||
"no-downloads": "Aucun téléchargement",
|
}
|
||||||
"download-queue": "File d’attente des téléchargements"
|
},
|
||||||
|
"extension": {
|
||||||
|
"title": "Extensions"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"empty": "Votre bibliothèque est vide"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Bibliothèque"
|
||||||
|
},
|
||||||
|
"manga": {
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"request_failure": "Impossible de charger le manga"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"title": "Sources"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
|
|
||||||
import en from 'i18n/locale/en.json';
|
import en from 'i18n/locale/en.json';
|
||||||
|
|
||||||
const translationHelper = (lng: any) => ({
|
const translationHelper = <T>(lng: T) => ({
|
||||||
translation: lng,
|
translation: lng,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: translationHelper(en),
|
en: translationHelper(en),
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export default resources;
|
export default resources;
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export default function Browse() {
|
|||||||
scrollButtons
|
scrollButtons
|
||||||
allowScrollButtonsMobile
|
allowScrollButtonsMobile
|
||||||
>
|
>
|
||||||
<Tab sx={{ textTransform: 'none' }} label={t('screens.Browse.sources')} />
|
<Tab sx={{ textTransform: 'none' }} label={t('source.title')} />
|
||||||
<Tab sx={{ textTransform: 'none' }} label={t('screens.Browse.extensions')} />
|
<Tab sx={{ textTransform: 'none' }} label={t('extension.title')} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<TabPanel index={0} currentIndex={tabNum}>
|
<TabPanel index={0} currentIndex={tabNum}>
|
||||||
<Sources />
|
<Sources />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const DownloadQueue: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('screens.DownloadQueue.download-queue'));
|
setTitle(t('download.queue.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ const DownloadQueue: React.FC = () => {
|
|||||||
const onDragEnd = (result: DropResult) => {};
|
const onDragEnd = (result: DropResult) => {};
|
||||||
|
|
||||||
if (queue.length === 0) {
|
if (queue.length === 0) {
|
||||||
return <EmptyView message={t('screens.DownloadQueue.no-downloads')} />;
|
return <EmptyView message={t('download.queue.label.no_downloads')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (chapter: IChapter) => {
|
const handleDelete = (chapter: IChapter) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useQueryParam, StringParam } from 'use-query-params';
|
|||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { Typography, useMediaQuery, useTheme } from '@mui/material';
|
import { Typography, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import { IExtension } from 'typings';
|
import { IExtension } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const LANGUAGE = 0;
|
const LANGUAGE = 0;
|
||||||
const EXTENSIONS = 1;
|
const EXTENSIONS = 1;
|
||||||
@@ -66,6 +67,8 @@ function groupExtensions(extensions: IExtension[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MangaExtensions() {
|
export default function MangaExtensions() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||||
@@ -75,7 +78,7 @@ export default function MangaExtensions() {
|
|||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Extensions');
|
setTitle(t('extension.title'));
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch />
|
<AppbarSearch />
|
||||||
@@ -120,18 +123,18 @@ export default function MangaExtensions() {
|
|||||||
inputRef.current.value = '';
|
inputRef.current.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
makeToast('Installing Extension File....', 'info');
|
makeToast(t('extension.label.installing_file'), 'info');
|
||||||
client
|
client
|
||||||
.post('/api/v1/extension/install', formData, {
|
.post('/api/v1/extension/install', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
makeToast('Installed extension successfully!', 'success');
|
makeToast(t('extension.label.installed_successfully'), 'success');
|
||||||
mutate();
|
mutate();
|
||||||
})
|
})
|
||||||
.catch(() => makeToast('Extension installion failed!', 'error'));
|
.catch(() => makeToast(t('extension.label.installation_failed'), 'error'));
|
||||||
} else {
|
} else {
|
||||||
makeToast('invalid file type!', 'error');
|
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function Library() {
|
|||||||
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('screens.Library.Library'));
|
setTitle(t('library.title'));
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch />
|
<AppbarSearch />
|
||||||
@@ -62,7 +62,7 @@ export default function Library() {
|
|||||||
if (tabsError != null) {
|
if (tabsError != null) {
|
||||||
return (
|
return (
|
||||||
<EmptyView
|
<EmptyView
|
||||||
message={t('screens.Library.could-not-load-categories')}
|
message={t('category.error.label.request_failure')}
|
||||||
messageExtra={tabsError?.message ?? tabsError}
|
messageExtra={tabsError?.message ?? tabsError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -73,7 +73,7 @@ export default function Library() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
if (tabs.length === 0) {
|
||||||
return <EmptyView message={t('screens.Library.your-library-is-empty')} />;
|
return <EmptyView message={t('library.error.label.empty')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tabs.length === 1) {
|
if (tabs.length === 1) {
|
||||||
@@ -81,7 +81,7 @@ export default function Library() {
|
|||||||
<LibraryMangaGrid
|
<LibraryMangaGrid
|
||||||
mangas={mangas}
|
mangas={mangas}
|
||||||
lastLibraryUpdate={lastLibraryUpdate}
|
lastLibraryUpdate={lastLibraryUpdate}
|
||||||
message={t('screens.Library.your-library-is-empty') as string}
|
message={t('library.error.label.empty') as string}
|
||||||
isLoading={activeTab != null && mangaLoading}
|
isLoading={activeTab != null && mangaLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -112,14 +112,14 @@ export default function Library() {
|
|||||||
{tab === activeTab &&
|
{tab === activeTab &&
|
||||||
(mangaError ? (
|
(mangaError ? (
|
||||||
<EmptyView
|
<EmptyView
|
||||||
message={t('screens.Library.could-not-load-manga')}
|
message={t('manga.error.label.request_failure')}
|
||||||
messageExtra={mangaError?.message ?? mangaError}
|
messageExtra={mangaError?.message ?? mangaError}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LibraryMangaGrid
|
<LibraryMangaGrid
|
||||||
mangas={mangas}
|
mangas={mangas}
|
||||||
lastLibraryUpdate={lastLibraryUpdate}
|
lastLibraryUpdate={lastLibraryUpdate}
|
||||||
message={t('screens.Library.category-is-empty') as string}
|
message={t('library.error.label.empty') as string}
|
||||||
isLoading={mangaLoading}
|
isLoading={mangaLoading}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { NavbarToolbar } from 'components/navbar/DefaultNavBar';
|
|||||||
import EmptyView from 'components/util/EmptyView';
|
import EmptyView from 'components/util/EmptyView';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
import React, { useContext, useEffect, useRef } from 'react';
|
import React, { useContext, useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useQuery } from 'util/client';
|
import { useQuery } from 'util/client';
|
||||||
import { IManga } from 'typings';
|
import { IManga } from 'typings';
|
||||||
@@ -24,6 +25,8 @@ import { IManga } from 'typings';
|
|||||||
const AUTOFETCH_AGE = 60 * 60 * 24; // 24 hours
|
const AUTOFETCH_AGE = 60 * 60 * 24; // 24 hours
|
||||||
|
|
||||||
const Manga: React.FC = () => {
|
const Manga: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { setTitle } = useContext(NavbarContext);
|
const { setTitle } = useContext(NavbarContext);
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const autofetchedRef = useRef(false);
|
const autofetchedRef = useRef(false);
|
||||||
@@ -58,11 +61,11 @@ const Manga: React.FC = () => {
|
|||||||
}, [manga]);
|
}, [manga]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(manga?.title ?? 'Manga');
|
setTitle(manga?.title ?? t('manga.title'));
|
||||||
}, [manga?.title]);
|
}, [manga?.title]);
|
||||||
|
|
||||||
if (error && !manga) {
|
if (error && !manga) {
|
||||||
return <EmptyView message="Could not load manga" messageExtra={error.message ?? error} />;
|
return <EmptyView message={t('manga.error.label.request_failure')} messageExtra={error.message ?? error} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
||||||
@@ -72,7 +75,7 @@ const Manga: React.FC = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Could not fetch manga data
|
{t('manga.error.label.request_failure')}
|
||||||
<br />
|
<br />
|
||||||
{error.message ?? error}
|
{error.message ?? error}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from 'util/readerSettings';
|
} from 'util/readerSettings';
|
||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
import { IChapter, IManga, IMangaCard, IPartialChapter, IReaderSettings, ReaderType } from 'typings';
|
import { IChapter, IManga, IMangaCard, IPartialChapter, IReaderSettings, ReaderType } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const getReaderComponent = (readerType: ReaderType) => {
|
const getReaderComponent = (readerType: ReaderType) => {
|
||||||
switch (readerType) {
|
switch (readerType) {
|
||||||
@@ -62,6 +63,7 @@ const initialChapter = () => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function Reader() {
|
export default function Reader() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
@@ -87,13 +89,13 @@ export default function Reader() {
|
|||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||||
setSettings({ ...settings, [key]: value });
|
setSettings({ ...settings, [key]: value });
|
||||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
||||||
makeToast('Failed to save the reader settings to the server', 'warning'),
|
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!manga?.title || (chapter as IChapter)?.name === 'Loading...') {
|
if (!manga?.title || (chapter as IChapter)?.name === t('global.label.loading')) {
|
||||||
setTitle(`Reader - Manga ${mangaId} Chapter ${chapterIndex}`);
|
setTitle(t('reader.title'));
|
||||||
} else {
|
} else {
|
||||||
setTitle(`${manga.title}: ${(chapter as IChapter).name}`);
|
setTitle(`${manga.title}: ${(chapter as IChapter).name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import client from 'util/client';
|
|||||||
import { langCodeToName, langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from 'util/language';
|
import { langCodeToName, langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from 'util/language';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import { ISource } from 'typings';
|
import { ISource } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function sourceToLangList(sources: ISource[]) {
|
function sourceToLangList(sources: ISource[]) {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
@@ -34,6 +35,8 @@ function sourceToLangList(sources: ISource[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SearchAll: React.FC = () => {
|
const SearchAll: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
||||||
@@ -53,7 +56,7 @@ const SearchAll: React.FC = () => {
|
|||||||
const limit = new PQueue({ concurrency: 5 });
|
const limit = new PQueue({ concurrency: 5 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Global Search');
|
setTitle(t('search.title.global_search'));
|
||||||
setAction(<AppbarSearch />);
|
setAction(<AppbarSearch />);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -146,7 +149,7 @@ const SearchAll: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Sources');
|
setTitle(t('source.title'));
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch autoOpen />
|
<AppbarSearch autoOpen />
|
||||||
@@ -210,7 +213,7 @@ const SearchAll: React.FC = () => {
|
|||||||
setLastPageNum={setLastPageNum}
|
setLastPageNum={setLastPageNum}
|
||||||
horizontal
|
horizontal
|
||||||
noFaces
|
noFaces
|
||||||
message={fetched[id] ? 'No manga was found!' : undefined}
|
message={fetched[id] ? t('manga.error.label.no_mangas_found') : undefined}
|
||||||
inLibraryIndicator
|
inLibraryIndicator
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -36,11 +36,14 @@ import DarkTheme from 'components/context/DarkTheme';
|
|||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import ListItemLink from 'components/util/ListItemLink';
|
import ListItemLink from 'components/util/ListItemLink';
|
||||||
import SearchSettings from 'screens/settings/SearchSettings';
|
import SearchSettings from 'screens/settings/SearchSettings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Settings');
|
setTitle(t('settings.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -99,25 +102,25 @@ export default function Settings() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ListAltIcon />
|
<ListAltIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Categories" />
|
<ListItemText primary={t('category.title.categories')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to="/settings/defaultReaderSettings">
|
<ListItemLink to="/settings/defaultReaderSettings">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<AutoStoriesIcon />
|
<AutoStoriesIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Default Reader Settings" />
|
<ListItemText primary={t('reader.settings.title.default_reader_settings')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to="/settings/backup">
|
<ListItemLink to="/settings/backup">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<BackupIcon />
|
<BackupIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Backup" />
|
<ListItemText primary={t('settings.backup.title')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Brightness6Icon />
|
<Brightness6Icon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Dark Theme" />
|
<ListItemText primary={t('settings.label.dark_theme')} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch edge="end" checked={darkTheme} onChange={() => setDarkTheme(!darkTheme)} />
|
<Switch edge="end" checked={darkTheme} onChange={() => setDarkTheme(!darkTheme)} />
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
@@ -128,7 +131,7 @@ export default function Settings() {
|
|||||||
<ViewModuleIcon />
|
<ViewModuleIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Manga Item width"
|
primary={t('settings.label.manga_item_width')}
|
||||||
secondary={`px:${ItemWidth}`}
|
secondary={`px:${ItemWidth}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDialogOpenItemWidth();
|
handleDialogOpenItemWidth();
|
||||||
@@ -139,7 +142,10 @@ export default function Settings() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<FavoriteIcon />
|
<FavoriteIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Show NSFW" secondary="Hide NSFW extensions and sources" />
|
<ListItemText
|
||||||
|
primary={t('settings.label.show_nsfw')}
|
||||||
|
secondary={t('settings.label.show_nsfw_description')}
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch edge="end" checked={showNsfw} onChange={() => setShowNsfw(!showNsfw)} />
|
<Switch edge="end" checked={showNsfw} onChange={() => setShowNsfw(!showNsfw)} />
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
@@ -149,9 +155,8 @@ export default function Settings() {
|
|||||||
<CachedIcon />
|
<CachedIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Use image cache"
|
primary={t('settings.label.image_cache')}
|
||||||
secondary="Disabling image cache makes images load faster if you have a slow disk,
|
secondary={t('settings.label.image_cache_description')}
|
||||||
but uses it much more internet traffic in turn"
|
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch edge="end" checked={useCache} onChange={() => setUseCache(!useCache)} />
|
<Switch edge="end" checked={useCache} onChange={() => setUseCache(!useCache)} />
|
||||||
@@ -161,7 +166,7 @@ export default function Settings() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DnsIcon />
|
<DnsIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Server Address" secondary={serverAddress} />
|
<ListItemText primary={t('settings.about.label.server_address')} secondary={serverAddress} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -177,18 +182,18 @@ export default function Settings() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="About" />
|
<ListItemText primary={t('settings.about.title')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>Enter Server Address</DialogContentText>
|
<DialogContentText>{t('settings.server_address.dialog.label.enter_address')}</DialogContentText>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="name"
|
id="name"
|
||||||
label="Server Address"
|
label={t('settings.about.label.server_address')}
|
||||||
type="text"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={dialogValue}
|
value={dialogValue}
|
||||||
@@ -198,16 +203,16 @@ export default function Settings() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleDialogCancel} color="primary">
|
<Button onClick={handleDialogCancel} color="primary">
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDialogSubmit} color="primary">
|
<Button onClick={handleDialogSubmit} color="primary">
|
||||||
Set
|
{t('global.button.set')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
|
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
|
||||||
<DialogTitle>Manga Item width</DialogTitle>
|
<DialogTitle>{t('settings.label.manga_item_width')}</DialogTitle>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{
|
sx={{
|
||||||
width: '98%',
|
width: '98%',
|
||||||
@@ -236,13 +241,13 @@ export default function Settings() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleDialogResetItemWidth} color="primary">
|
<Button onClick={handleDialogResetItemWidth} color="primary">
|
||||||
Reset to Default
|
{t('global.button.reset_to_default')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDialogCancelItemWidth} color="primary">
|
<Button onClick={handleDialogCancelItemWidth} color="primary">
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDialogSubmitItemWidth} color="primary">
|
<Button onClick={handleDialogSubmitItemWidth} color="primary">
|
||||||
OK
|
{t('global.button.ok')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelec
|
|||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import cloneObject from 'util/cloneObject';
|
import cloneObject from 'util/cloneObject';
|
||||||
import { SourcePreferences } from 'typings';
|
import { SourcePreferences } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function getPrefComponent(type: string) {
|
function getPrefComponent(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -35,10 +36,11 @@ function getPrefComponent(type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SourceConfigure() {
|
export default function SourceConfigure() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Source Configuration');
|
setTitle(t('source.configuration.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useQueryParam, StringParam } from 'use-query-params';
|
|||||||
import SourceGridLayout from 'components/source/GridLayouts';
|
import SourceGridLayout from 'components/source/GridLayouts';
|
||||||
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
import { IManga, IMangaCard, ISource, ISourceFilters } from 'typings';
|
import { IManga, IMangaCard, ISource, ISourceFilters } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface IPos {
|
interface IPos {
|
||||||
position: number;
|
position: number;
|
||||||
@@ -26,6 +27,7 @@ interface IPos {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SourceMangas({ popular }: { popular: boolean }) {
|
export default function SourceMangas({ popular }: { popular: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Source'); // title is later set after a fetch but we set it here once
|
setTitle(t('source.title')); // title is later set after a fetch but we set it here once
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -220,12 +222,14 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
|
|||||||
let messageExtra;
|
let messageExtra;
|
||||||
|
|
||||||
if (fetched) {
|
if (fetched) {
|
||||||
message = 'No manga was found!';
|
message = t('manga.error.label.no_mangas_found');
|
||||||
if (sourceId === '0') {
|
if (sourceId === '0') {
|
||||||
messageExtra = (
|
messageExtra = (
|
||||||
<>
|
<>
|
||||||
<span>Check out </span>
|
<span>{t('source.local_source.label.checkout')} </span>
|
||||||
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">Local source guide</a>
|
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">
|
||||||
|
{t('source.local_source.label.guide')}
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import TravelExploreIcon from '@mui/icons-material/TravelExplore';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useQuery } from 'util/client';
|
import { useQuery } from 'util/client';
|
||||||
import { ISource } from 'typings';
|
import { ISource } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function sourceToLangList(sources: ISource[]) {
|
function sourceToLangList(sources: ISource[]) {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
@@ -44,6 +45,7 @@ function groupByLang(sources: ISource[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Sources() {
|
export default function Sources() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||||
@@ -70,7 +72,7 @@ export default function Sources() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Sources');
|
setTitle(t('source.title'));
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<IconButton onClick={() => history.push('/sources/all/search/')} size="large">
|
<IconButton onClick={() => history.push('/sources/all/search/')} size="large">
|
||||||
@@ -89,7 +91,7 @@ export default function Sources() {
|
|||||||
if (loading) return <LoadingPlaceholder />;
|
if (loading) return <LoadingPlaceholder />;
|
||||||
|
|
||||||
if (sources?.length === 0) {
|
if (sources?.length === 0) {
|
||||||
return <h3>No sources found. Install Some Extensions first.</h3>;
|
return <h3>{t('source.error.label.no_sources_found')}</h3>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import { Link, useHistory } from 'react-router-dom';
|
|||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import { IChapter, IMangaChapter, IQueue, PaginatedList } from 'typings';
|
import { IChapter, IMangaChapter, IQueue, PaginatedList } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { t as translate } from 'i18next';
|
||||||
|
|
||||||
function epochToDate(epoch: number) {
|
function epochToDate(epoch: number) {
|
||||||
const date = new Date(0); // The 0 there is the key, which sets the date to the epoch
|
const date = new Date(0); // The 0 there is the key, which sets the date to the epoch
|
||||||
@@ -39,11 +41,11 @@ function isTheSameDay(first: Date, second: Date) {
|
|||||||
|
|
||||||
function getDateString(date: Date) {
|
function getDateString(date: Date) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
if (isTheSameDay(today, date)) return 'TODAY';
|
if (isTheSameDay(today, date)) return translate('global.date.label.today');
|
||||||
// calculate yesterday
|
// calculate yesterday
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
yesterday.setDate(today.getDate() - 1);
|
yesterday.setDate(today.getDate() - 1);
|
||||||
if (isTheSameDay(yesterday, date)) return 'YESTERDAY';
|
if (isTheSameDay(yesterday, date)) return translate('global.date.label.yesterday');
|
||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ const initialQueue = {
|
|||||||
} as IQueue;
|
} as IQueue;
|
||||||
|
|
||||||
const Updates: React.FC = () => {
|
const Updates: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
@@ -97,7 +100,7 @@ const Updates: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Updates');
|
setTitle(t('updates.title'));
|
||||||
|
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -136,7 +139,7 @@ const Updates: React.FC = () => {
|
|||||||
return <LoadingPlaceholder />;
|
return <LoadingPlaceholder />;
|
||||||
}
|
}
|
||||||
if (fetched && updateEntries.length === 0) {
|
if (fetched && updateEntries.length === 0) {
|
||||||
return <EmptyView message="You don't have any updates yet." />;
|
return <EmptyView message={t('updates.error.label.no_updates_available')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadForChapter = (chapter: IChapter) => {
|
const downloadForChapter = (chapter: IChapter) => {
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import NavbarContext from 'components/context/NavbarContext';
|
|||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
import { useQuery } from 'util/client';
|
import { useQuery } from 'util/client';
|
||||||
import { IAbout } from 'typings';
|
import { IAbout } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('About');
|
setTitle(t('settings.about.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -32,19 +34,22 @@ export default function About() {
|
|||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Server" secondary={`${about.name} ${about.buildType}`} />
|
<ListItemText
|
||||||
|
primary={t('settings.about.label.server')}
|
||||||
|
secondary={`${about.name} ${about.buildType}`}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Server version" secondary={version()} />
|
<ListItemText primary={t('settings.about.label.server_version')} secondary={version()} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Build time" secondary={buildTime()} />
|
<ListItemText primary={t('settings.about.label.build_time')} secondary={buildTime()} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItemLink to={about.github}>
|
<ListItemLink to={about.github}>
|
||||||
<ListItemText primary="Github" secondary={about.github} />
|
<ListItemText primary={t('settings.about.label.github')} secondary={about.github} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to={about.discord}>
|
<ListItemLink to={about.discord}>
|
||||||
<ListItemText primary="Discord" secondary={about.discord} />
|
<ListItemText primary={t('settings.about.label.discord')} secondary={about.discord} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import client from 'util/client';
|
|||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
import ListItemLink from 'components/util/ListItemLink';
|
import ListItemLink from 'components/util/ListItemLink';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function Backup() {
|
export default function Backup() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Backup');
|
setTitle(t('settings.backup.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -29,17 +31,17 @@ export default function Backup() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('backup.proto.gz', file);
|
formData.append('backup.proto.gz', file);
|
||||||
|
|
||||||
makeToast('Restoring backup....', 'info');
|
makeToast(t('settings.backup.label.restoring_backup'), 'info');
|
||||||
client
|
client
|
||||||
.post('/api/v1/backup/import/file', formData, {
|
.post('/api/v1/backup/import/file', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
})
|
})
|
||||||
.then(() => makeToast('Backup restore finished!', 'success'))
|
.then(() => makeToast(t('settings.backup.label.restored_backup'), 'success'))
|
||||||
.catch(() => makeToast('Backup restore failed!', 'error'));
|
.catch(() => makeToast(t('settings.backup.label.backup_restore_failed'), 'error'));
|
||||||
} else if (file.name.toLowerCase().endsWith('json')) {
|
} else if (file.name.toLowerCase().endsWith('json')) {
|
||||||
makeToast('legacy backups are not supported!', 'error');
|
makeToast(t('settings.backup.label.legacy_backup_unsupported'), 'error');
|
||||||
} else {
|
} else {
|
||||||
makeToast('invalid file type!', 'error');
|
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,12 +76,15 @@ export default function Backup() {
|
|||||||
<>
|
<>
|
||||||
<List sx={{ padding: 0 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
<ListItemLink to={`${baseURL}/api/v1/backup/export/file`} directLink>
|
<ListItemLink to={`${baseURL}/api/v1/backup/export/file`} directLink>
|
||||||
<ListItemText primary="Create Backup" secondary="Backup library as a Tachiyomi backup" />
|
<ListItemText
|
||||||
|
primary={t('settings.backup.label.create_backup')}
|
||||||
|
secondary={t('settings.backup.label.create_backup_info')}
|
||||||
|
/>
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItem button onClick={() => document.getElementById('backup-file')?.click()}>
|
<ListItem button onClick={() => document.getElementById('backup-file')?.click()}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Restore Backup"
|
primary={t('settings.backup.label.restore_backup')}
|
||||||
secondary="You can also drag and drop the backup file here to restore"
|
secondary={t('settings.backup.label.restore_backup_info')}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
|||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import client, { useQuery } from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import { ICategory } from 'typings';
|
import { ICategory } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const getItemStyle = (
|
const getItemStyle = (
|
||||||
isDragging: boolean,
|
isDragging: boolean,
|
||||||
@@ -49,9 +50,11 @@ const getItemStyle = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function Categories() {
|
export default function Categories() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Categories');
|
setTitle(t('category.title.categories'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -196,14 +199,16 @@ export default function Categories() {
|
|||||||
</Fab>
|
</Fab>
|
||||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||||
<DialogTitle id="form-dialog-title">
|
<DialogTitle id="form-dialog-title">
|
||||||
{categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
|
{categoryToEdit === -1
|
||||||
|
? t('category.dialog.title.new_category')
|
||||||
|
: t('category.dialog.title.edit_category')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="name"
|
id="name"
|
||||||
label="Category Name"
|
label={t('category.label.category_name')}
|
||||||
type="text"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={dialogName}
|
value={dialogName}
|
||||||
@@ -217,15 +222,15 @@ export default function Categories() {
|
|||||||
color="default"
|
color="default"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Default category when adding new manga to library"
|
label={t('category.label.use_as_default_category')}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleDialogCancel} color="primary">
|
<Button onClick={handleDialogCancel} color="primary">
|
||||||
Cancel
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDialogSubmit} color="primary">
|
<Button onClick={handleDialogSubmit} color="primary">
|
||||||
Submit
|
{t('global.button.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ import {
|
|||||||
} from 'util/readerSettings';
|
} from 'util/readerSettings';
|
||||||
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
|
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
|
||||||
import { IReaderSettings } from 'typings';
|
import { IReaderSettings } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function DefaultReaderSettings() {
|
export default function DefaultReaderSettings() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Default Reader Settings');
|
setTitle(t('reader.settings.title.default_reader_settings'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ export default function DefaultReaderSettings() {
|
|||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
||||||
makeToast('Failed to save the default reader settings to the server', 'warning'),
|
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import makeToast from 'components/util/Toast';
|
|||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import { SearchMetadataKeys } from 'typings';
|
import { SearchMetadataKeys } from 'typings';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function SearchSettings() {
|
export default function SearchSettings() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { metadata, settings } = useSearchSettings();
|
const { metadata, settings } = useSearchSettings();
|
||||||
|
|
||||||
const setSettingValue = (key: SearchMetadataKeys, value: boolean) => {
|
const setSettingValue = (key: SearchMetadataKeys, value: boolean) => {
|
||||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
||||||
makeToast('Failed to save the default search settings to the server', 'warning'),
|
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +23,7 @@ export default function SearchSettings() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Ignore Filters when Searching" />
|
<ListItemText primary={t('search.label.ignore_filters')} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
|
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
|
||||||
|
import { TFuncKey } from 'i18next';
|
||||||
|
|
||||||
|
export type TranslationKey = TFuncKey;
|
||||||
|
|
||||||
export interface IExtension {
|
export interface IExtension {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -301,7 +304,7 @@ export interface SourcePreferences {
|
|||||||
|
|
||||||
export interface NavbarItem {
|
export interface NavbarItem {
|
||||||
path: string;
|
path: string;
|
||||||
title: string;
|
title: TranslationKey;
|
||||||
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
||||||
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
||||||
show: 'mobile' | 'desktop' | 'both';
|
show: 'mobile' | 'desktop' | 'both';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
export const isWithinLastXMillis = (date: Date, timeMS: number) => {
|
export const isWithinLastXMillis = (date: Date, timeMS: number) => {
|
||||||
const timeDifference = Date.now() - date.getTime();
|
const timeDifference = Date.now() - date.getTime();
|
||||||
@@ -54,11 +55,11 @@ export const getUploadDateString = (date: Date | number) => {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (wasUploadedToday) {
|
if (wasUploadedToday) {
|
||||||
return `Today at ${timeString}`;
|
return t('global.date.label.today_at', { timeString });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasUploadedYesterday) {
|
if (wasUploadedYesterday) {
|
||||||
return `Yesterday at ${timeString}`;
|
return t('global.date.label.yesterday_at', { timeString });
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadDate.toLocaleDateString(undefined, {
|
return uploadDate.toLocaleDateString(undefined, {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
export const ISOLanguages = [
|
export const ISOLanguages = [
|
||||||
{ code: 'all', name: 'All', nativeName: 'All' },
|
{ code: 'all', name: 'All', nativeName: 'All' },
|
||||||
@@ -80,7 +81,7 @@ export function langCodeToName(code: string): string {
|
|||||||
const whereToCut = code.indexOf('-') !== -1 ? code.indexOf('-') : code.length;
|
const whereToCut = code.indexOf('-') !== -1 ? code.indexOf('-') : code.length;
|
||||||
|
|
||||||
const proccessedCode = code.toLocaleLowerCase().substring(0, whereToCut);
|
const proccessedCode = code.toLocaleLowerCase().substring(0, whereToCut);
|
||||||
let result = `language with code: ${code}`;
|
let result = t('global.language.label.language_with_code', { code });
|
||||||
|
|
||||||
for (let i = 0; i < ISOLanguages.length; i++) {
|
for (let i = 0; i < ISOLanguages.length; i++) {
|
||||||
if (ISOLanguages[i].code === proccessedCode || ISOLanguages[i].code === code.toLocaleLowerCase()) {
|
if (ISOLanguages[i].code === proccessedCode || ISOLanguages[i].code === code.toLocaleLowerCase()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user