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 useLocalStorage from 'util/useLocalStorage';
|
||||
import { Box } from '@mui/system';
|
||||
import { IExtension } from 'typings';
|
||||
import { IExtension, TranslationKey } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
extension: IExtension;
|
||||
@@ -50,7 +51,19 @@ const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]: Extens
|
||||
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
|
||||
} 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) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
extension: { name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw },
|
||||
notifyInstall,
|
||||
@@ -68,7 +81,7 @@ export default function ExtensionCard(props: IProps) {
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
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 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' }}
|
||||
onClick={() => handleButtonClick()}
|
||||
>
|
||||
{installedState}
|
||||
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Box, styled } from '@mui/system';
|
||||
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||
import { BACK } from 'util/useBackTo';
|
||||
import { IMangaCard } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BottomGradient = styled('div')({
|
||||
position: 'absolute',
|
||||
@@ -73,6 +74,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
manga: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -131,7 +134,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
||||
>
|
||||
{inLibraryIndicator && inLibrary && (
|
||||
<Typography sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}>
|
||||
In library
|
||||
{t('manga.button.in_library')}
|
||||
</Typography>
|
||||
)}
|
||||
{showUnreadBadge && unread! > 0 && (
|
||||
@@ -252,7 +255,9 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
||||
</Box>
|
||||
<BadgeContainer>
|
||||
{inLibraryIndicator && inLibrary && (
|
||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>In library</Typography>
|
||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||
{t('manga.button.in_library')}
|
||||
</Typography>
|
||||
)}
|
||||
{showUnreadBadge && unread! > 0 && (
|
||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>{unread}</Typography>
|
||||
|
||||
@@ -13,6 +13,7 @@ import CardContent from '@mui/material/CardContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Box, styled } from '@mui/system';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { langCodeToName } from 'util/language';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
@@ -41,6 +42,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
source: { id, name, lang, iconUrl, supportsLatest, isNsfw },
|
||||
} = props;
|
||||
@@ -110,18 +113,18 @@ const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||
<MobileWidthButtons>
|
||||
{supportsLatest && (
|
||||
<Button variant="outlined" onClick={(e) => redirectTo(e, `/sources/${id}/latest/`)}>
|
||||
Latest
|
||||
{t('global.button.latest')}
|
||||
</Button>
|
||||
)}
|
||||
</MobileWidthButtons>
|
||||
<WiderWidthButtons>
|
||||
{supportsLatest && (
|
||||
<Button component={Link} to={`/sources/${id}/latest/`} variant="outlined">
|
||||
Latest
|
||||
{t('global.button.latest')}
|
||||
</Button>
|
||||
)}
|
||||
<Button component={Link} to={`/sources/${id}/popular/`} variant="outlined">
|
||||
Browse
|
||||
{t('global.button.browse')}
|
||||
</Button>
|
||||
</WiderWidthButtons>
|
||||
</>
|
||||
|
||||
@@ -13,8 +13,7 @@ import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
import { IMangaCard, LibrarySortMode, NullAndUndefined } from 'typings';
|
||||
import { useSearchSettings } from 'util/searchSettings';
|
||||
|
||||
const FILTERED_OUT_MESSAGE = 'There are no Manga matching this filter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IMangaCard): boolean => {
|
||||
switch (unread) {
|
||||
@@ -118,6 +117,8 @@ const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: nu
|
||||
message,
|
||||
lastLibraryUpdate,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
const { options } = useLibraryOptionsContext();
|
||||
const { unread, downloaded } = options;
|
||||
@@ -155,7 +156,7 @@ const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: nu
|
||||
hasNextPage={lastPageNum < totalPages}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={showFilteredOutMessage ? FILTERED_OUT_MESSAGE : message}
|
||||
message={showFilteredOutMessage ? t('library.error.label.no_matches') : message}
|
||||
gridLayout={options.gridLayout}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,19 +13,20 @@ import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
||||
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||
import OptionsTabs from 'components/molecules/OptionsTabs';
|
||||
import React from 'react';
|
||||
import { LibraryOptions, LibrarySortMode } from 'typings';
|
||||
import { LibraryOptions, LibrarySortMode, TranslationKey } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const TITLES = {
|
||||
filter: 'Filter',
|
||||
sort: 'Sort',
|
||||
display: 'Display',
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
sort: 'global.label.sort',
|
||||
display: 'global.label.display',
|
||||
};
|
||||
|
||||
const SORT_OPTIONS: [LibrarySortMode, string][] = [
|
||||
['sortToRead', 'By Unread chapters'],
|
||||
['sortAlph', 'Alphabetically'],
|
||||
['sortDateAdded', 'Date Added'],
|
||||
['sortLastRead', 'Last Read'],
|
||||
const SORT_OPTIONS: [LibrarySortMode, TranslationKey][] = [
|
||||
['sortToRead', 'library.option.sort.label.by_unread_chapters'],
|
||||
['sortAlph', 'library.option.sort.label.alphabetically'],
|
||||
['sortDateAdded', 'library.option.sort.label.by_date_added'],
|
||||
['sortLastRead', 'library.option.sort.label.by_last_read'],
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
@@ -34,6 +35,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { options, setOptions } = useLibraryOptionsContext();
|
||||
|
||||
const handleFilterChange = <T extends keyof LibraryOptions>(key: T, value: LibraryOptions[T]) => {
|
||||
@@ -45,18 +47,18 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
tabs={['filter', 'sort', 'display']}
|
||||
tabTitle={(key) => TITLES[key]}
|
||||
tabTitle={(key) => t(TITLES[key])}
|
||||
tabContent={(key) => {
|
||||
if (key === 'filter') {
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Unread"
|
||||
label={t('global.filter.label.unread')}
|
||||
checked={options.unread}
|
||||
onChange={(c) => handleFilterChange('unread', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Downloaded"
|
||||
label={t('global.filter.label.downloaded')}
|
||||
checked={options.downloaded}
|
||||
onChange={(c) => handleFilterChange('downloaded', c)}
|
||||
/>
|
||||
@@ -67,7 +69,7 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
||||
return SORT_OPTIONS.map(([mode, label]) => (
|
||||
<SortRadioInput
|
||||
key={mode}
|
||||
label={label}
|
||||
label={t(label) as string}
|
||||
checked={options.sorts === mode}
|
||||
sortDescending={options.sortDesc}
|
||||
onClick={() =>
|
||||
@@ -82,36 +84,36 @@ const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
||||
const { gridLayout, showDownloadBadge, showUnreadBadge } = options;
|
||||
return (
|
||||
<>
|
||||
<FormLabel>Display mode</FormLabel>
|
||||
<FormLabel>{t('global.grid_layout.title')}</FormLabel>
|
||||
<RadioGroup
|
||||
onChange={(e) => handleFilterChange('gridLayout', Number(e.target.value))}
|
||||
value={gridLayout}
|
||||
>
|
||||
<RadioInput
|
||||
label="Compact grid"
|
||||
label={t('global.grid_layout.label.compact_grid')}
|
||||
value={GridLayout.Compact}
|
||||
checked={gridLayout == null || gridLayout === GridLayout.Compact}
|
||||
/>
|
||||
<RadioInput
|
||||
label="Comfortable grid"
|
||||
label={t('global.grid_layout.label.comfortable_grid')}
|
||||
value={GridLayout.Comfortable}
|
||||
checked={gridLayout === GridLayout.Comfortable}
|
||||
/>
|
||||
<RadioInput
|
||||
label="List"
|
||||
label={t('global.grid_layout.label.list')}
|
||||
value={GridLayout.List}
|
||||
checked={gridLayout === GridLayout.List}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>Badges</FormLabel>
|
||||
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.badge.title')}</FormLabel>
|
||||
<CheckboxInput
|
||||
label="Unread Badges"
|
||||
label={t('library.option.display.badge.label.unread_badges')}
|
||||
checked={showUnreadBadge === true}
|
||||
onChange={() => handleFilterChange('showUnreadBadge', !showUnreadBadge)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label="Download Badges"
|
||||
label={t('library.option.display.badge.label.download_badges')}
|
||||
checked={showDownloadBadge === true}
|
||||
onChange={() => handleFilterChange('showDownloadBadge', !showDownloadBadge)}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Typography from '@mui/material/Typography';
|
||||
import client from 'util/client';
|
||||
import makeToast from 'components/util/Toast';
|
||||
import { IUpdateStatus } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProgressProps {
|
||||
progress: number;
|
||||
@@ -30,6 +31,8 @@ interface IUpdateCheckerProps {
|
||||
}
|
||||
|
||||
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
@@ -39,7 +42,7 @@ function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
||||
setProgress(0);
|
||||
await client.post('/api/v1/update/fetch');
|
||||
} catch (e) {
|
||||
makeToast('Checking for updates failed!', 'error');
|
||||
makeToast(t('global.error.label.update_failed'), 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import client from 'util/client';
|
||||
import { BACK } from 'util/useBackTo';
|
||||
import { getUploadDateString } from 'util/date';
|
||||
import { IChapter, IDownloadChapter } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
chapter: IChapter;
|
||||
@@ -41,6 +42,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
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' }}
|
||||
/>
|
||||
)}
|
||||
{showChapterNumber ? `Chapter ${chapter.chapterNumber}` : chapter.name}
|
||||
{showChapterNumber ? `${t('chapter.title')} ${chapter.chapterNumber}` : chapter.name}
|
||||
</Typography>
|
||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||
<Typography variant="caption">
|
||||
{getUploadDateString(chapter.uploadDate)}
|
||||
{isDownloaded && ' • Downloaded'}
|
||||
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -160,14 +162,14 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<ListItemIcon>
|
||||
<CheckBoxOutlineBlank fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Select</ListItemText>
|
||||
<ListItemText>{t('chapter.action.label.select')}</ListItemText>
|
||||
</MenuItem>
|
||||
{isDownloaded && (
|
||||
<MenuItem onClick={deleteChapter}>
|
||||
<ListItemIcon>
|
||||
<Delete fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
<ListItemText>{t('chapter.action.download.delete.label.action')}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{canBeDownloaded && (
|
||||
@@ -175,7 +177,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<ListItemIcon>
|
||||
<Download fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Download</ListItemText>
|
||||
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||
@@ -184,8 +186,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{chapter.bookmarked && 'Remove bookmark'}
|
||||
{!chapter.bookmarked && 'Add bookmark'}
|
||||
{chapter.bookmarked && t('chapter.action.bookmark.remove.label.action')}
|
||||
{!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
|
||||
@@ -194,15 +196,15 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
{!chapter.read && <Done fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{chapter.read && 'Mark as unread'}
|
||||
{!chapter.read && 'Mark as read'}
|
||||
{chapter.read && t('chapter.action.mark_as_read.remove.label.action')}
|
||||
{!chapter.read && t('chapter.action.mark_as_read.add.label.action.current')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
||||
<ListItemIcon>
|
||||
<DoneAll fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Mark previous as Read</ListItemText>
|
||||
<ListItemText>{t('chapter.action.mark_as_read.add.label.action.previous')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Card>
|
||||
|
||||
@@ -13,14 +13,14 @@ import ChapterCard from 'components/manga/ChapterCard';
|
||||
import ResumeFab from 'components/manga/ResumeFAB';
|
||||
import { filterAndSortChapters, useChapterOptions } from 'components/manga/util';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import { interpolate } from 'components/util/helpers';
|
||||
import makeToast from 'components/util/Toast';
|
||||
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import client, { useQuery } from 'util/client';
|
||||
import ChaptersToolbarMenu from 'components/manga/ChaptersToolbarMenu';
|
||||
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 }) => ({
|
||||
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: {
|
||||
success: { one: 'Download added', many: '%count% downloads added' },
|
||||
error: { one: 'Error adding download', many: 'Error adding downloads' },
|
||||
success: 'chapter.action.download.add.label.success',
|
||||
error: 'chapter.action.download.add.label.error',
|
||||
},
|
||||
delete: {
|
||||
success: { one: 'Chapter deleted', many: '%count% chapters deleted' },
|
||||
error: { one: 'Error deleting chapter', many: 'Error deleting chapters' },
|
||||
success: 'chapter.action.download.delete.label.success',
|
||||
error: 'chapter.action.download.delete.label.error',
|
||||
},
|
||||
bookmark: {
|
||||
success: { one: 'Chapter bookmarked', many: '%count% chapters bookmarked' },
|
||||
error: { one: 'Error bookmarking chapter', many: 'Error bookmarking chapters' },
|
||||
success: 'chapter.action.bookmark.add.label.success',
|
||||
error: 'chapter.action.bookmark.add.label.error',
|
||||
},
|
||||
unbookmark: {
|
||||
success: { one: 'Chapter bookmark removed', many: '%count% chapter bookmarks removed' },
|
||||
error: { one: 'Error removing bookmark', many: 'Error removing bookmarks' },
|
||||
success: 'chapter.action.bookmark.remove.label.success',
|
||||
error: 'chapter.action.bookmark.remove.label.error',
|
||||
},
|
||||
mark_as_read: {
|
||||
success: { one: 'Chapter marked as read', many: '%count% chapters marked as read' },
|
||||
error: { one: 'Error marking chapter as read', many: 'Error marking chapters as read' },
|
||||
success: 'chapter.action.mark_as_read.add.label.success',
|
||||
error: 'chapter.action.mark_as_read.add.label.error',
|
||||
},
|
||||
mark_as_unread: {
|
||||
success: { one: 'Chapter marked as unread', many: '%count% chapters marked as unread' },
|
||||
error: { one: 'Error marking chapter as unread', many: 'Error marking chapters as unread' },
|
||||
success: 'chapter.action.mark_as_read.remove.label.success',
|
||||
error: 'chapter.action.mark_as_read.remove.label.error',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -72,6 +77,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selection, setSelection] = useState<number[] | null>(null);
|
||||
const prevQueueRef = useRef<IDownloadChapter[]>();
|
||||
const queue = useSubscription<IQueue>('/api/v1/downloads').data?.queue;
|
||||
@@ -162,9 +169,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
}
|
||||
|
||||
actionPromise
|
||||
.then(() => makeToast(interpolate(chapterIds.length, actionsStrings[action].success), 'success'))
|
||||
.then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }) as string, 'success'))
|
||||
.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) {
|
||||
@@ -214,7 +221,9 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{`${visibleChapters.length} Chapter${visibleChapters.length === 1 ? '' : 's'}`}
|
||||
{`${visibleChapters.length} ${t('chapter.title', {
|
||||
count: visibleChapters.length,
|
||||
})}`}
|
||||
</Typography>
|
||||
|
||||
{selection === null ? (
|
||||
@@ -222,17 +231,17 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
) : (
|
||||
<Stack direction="row">
|
||||
<Button size="small" onClick={handleSelectAll}>
|
||||
Select all
|
||||
{t('global.button.select_all')}
|
||||
</Button>
|
||||
<Button size="small" onClick={handleClear}>
|
||||
Clear
|
||||
{t('global.button.clear')}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{noChaptersFound && <EmptyView message="No chapters found" />}
|
||||
{noChaptersMatchingFilter && <EmptyView message="No chapters matching filter" />}
|
||||
{noChaptersFound && <EmptyView message={t('chapter.error.label.no_chapter_found')} />}
|
||||
{noChaptersMatchingFilter && <EmptyView message={t('chapter.error.label.no_matches')} />}
|
||||
|
||||
<StyledVirtuoso
|
||||
style={{
|
||||
|
||||
@@ -12,7 +12,8 @@ import ThreeStateCheckboxInput from 'components/atoms/ThreeStateCheckboxInput';
|
||||
import OptionsTabs from 'components/molecules/OptionsTabs';
|
||||
import React from 'react';
|
||||
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 {
|
||||
open: boolean;
|
||||
@@ -21,88 +22,92 @@ interface IProps {
|
||||
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
|
||||
}
|
||||
|
||||
const TITLES = {
|
||||
filter: 'Filter',
|
||||
sort: 'Sort',
|
||||
display: 'Display',
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
sort: 'global.label.sort',
|
||||
display: 'global.label.display',
|
||||
};
|
||||
|
||||
const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => (
|
||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
minHeight={150}
|
||||
tabs={['filter', 'sort', 'display']}
|
||||
tabTitle={(key) => TITLES[key]}
|
||||
tabContent={(key) => {
|
||||
if (key === 'filter') {
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Unread"
|
||||
checked={options.unread}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'unread',
|
||||
filterValue: c,
|
||||
})
|
||||
const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
minHeight={150}
|
||||
tabs={['filter', 'sort', 'display']}
|
||||
tabTitle={(key) => t(TITLES[key])}
|
||||
tabContent={(key) => {
|
||||
if (key === 'filter') {
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.unread')}
|
||||
checked={options.unread}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'unread',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.downloaded')}
|
||||
checked={options.downloaded}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'downloaded',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.bookmarked')}
|
||||
checked={options.bookmarked}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'bookmarked',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (key === 'sort') {
|
||||
return SORT_OPTIONS.map(([mode, label]) => (
|
||||
<SortRadioInput
|
||||
key={mode}
|
||||
label={t(label) as string}
|
||||
checked={options.sortBy === mode}
|
||||
sortDescending={options.reverse}
|
||||
onClick={() =>
|
||||
mode !== options.sortBy
|
||||
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
||||
: optionsDispatch({ type: 'sortReverse' })
|
||||
}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Downloaded"
|
||||
checked={options.downloaded}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'downloaded',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label="Bookmarked"
|
||||
checked={options.bookmarked}
|
||||
onChange={(c) =>
|
||||
optionsDispatch({
|
||||
type: 'filter',
|
||||
filterType: 'bookmarked',
|
||||
filterValue: c,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (key === 'sort') {
|
||||
return SORT_OPTIONS.map(([mode, label]) => (
|
||||
<SortRadioInput
|
||||
key={mode}
|
||||
label={label}
|
||||
checked={options.sortBy === mode}
|
||||
sortDescending={options.reverse}
|
||||
onClick={() =>
|
||||
mode !== options.sortBy
|
||||
? optionsDispatch({ type: 'sortBy', sortBy: mode })
|
||||
: optionsDispatch({ type: 'sortReverse' })
|
||||
}
|
||||
/>
|
||||
));
|
||||
}
|
||||
if (key === 'display') {
|
||||
return (
|
||||
<RadioGroup
|
||||
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
|
||||
value={options.showChapterNumber}
|
||||
>
|
||||
<RadioInput label="Source Title" value={false} />
|
||||
<RadioInput label="Chapter Number" value />
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
));
|
||||
}
|
||||
if (key === 'display') {
|
||||
return (
|
||||
<RadioGroup
|
||||
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
|
||||
value={options.showChapterNumber}
|
||||
>
|
||||
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} />
|
||||
<RadioInput label={t('chapter.option.display.label.chapter_number')} value />
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterOptions;
|
||||
|
||||
@@ -13,6 +13,7 @@ import IconButton from '@mui/material/IconButton';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { mutate } from 'swr';
|
||||
import client from 'util/client';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
@@ -129,6 +130,8 @@ function getValueOrUnknown(val: string) {
|
||||
}
|
||||
|
||||
const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||
|
||||
@@ -158,15 +161,15 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
<div className={classes.rightSide}>
|
||||
<h1>{manga.title}</h1>
|
||||
<h3>
|
||||
{'Author: '}
|
||||
{`${t('manga.label.author')}: `}
|
||||
<span>{getValueOrUnknown(manga.author)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
{'Artist: '}
|
||||
{`${t('manga.label.artist')}: `}
|
||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||
</h3>
|
||||
<h3>{`Status: ${manga.status}`}</h3>
|
||||
<h3>{`Source: ${getSourceName(manga.source)}`}</h3>
|
||||
<h3>{`${t('manga.label.status')}: ${manga.status}`}</h3>
|
||||
<h3>{`${t('source.title')}: ${getSourceName(manga.source)}`}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.buttons}>
|
||||
@@ -174,21 +177,23 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
<IconButton onClick={manga.inLibrary ? removeFromLibrary : addToLibrary} size="large">
|
||||
{manga.inLibrary ? <FavoriteIcon sx={{ mr: 1 }} /> : <FavoriteBorderIcon sx={{ mr: 1 }} />}
|
||||
<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>
|
||||
</IconButton>
|
||||
</div>
|
||||
<a href={manga.realUrl} target="_blank" rel="noreferrer">
|
||||
<IconButton size="large">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div className={classes.description}>
|
||||
<h4>About</h4>
|
||||
<h4>{t('settings.about.title')}</h4>
|
||||
<p>{manga.description}</p>
|
||||
</div>
|
||||
<div className={classes.genre}>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import CategorySelect from 'components/navbar/action/CategorySelect';
|
||||
import React, { useState } from 'react';
|
||||
import { IManga } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
manga: IManga;
|
||||
@@ -29,6 +30,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
|
||||
@@ -44,7 +46,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<>
|
||||
{isLargeScreen && (
|
||||
<>
|
||||
<Tooltip title="Reload data from source">
|
||||
<Tooltip title={t('manga.label.reload_from_source')}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onRefresh();
|
||||
@@ -55,7 +57,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{manga.inLibrary && (
|
||||
<Tooltip title="Edit manga categories">
|
||||
<Tooltip title={t('manga.label.edit_categories')}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setEditCategories(true);
|
||||
@@ -97,7 +99,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<ListItemIcon>
|
||||
<Refresh fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Reload data from source</ListItemText>
|
||||
<ListItemText>{t('manga.label.reload_from_source')}</ListItemText>
|
||||
</MenuItem>
|
||||
{manga.inLibrary && (
|
||||
<MenuItem
|
||||
@@ -109,7 +111,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<ListItemIcon>
|
||||
<Label fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit manga categories</ListItemText>
|
||||
<ListItemText>{t('manga.label.edit_categories')}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Link } from 'react-router-dom';
|
||||
import { PlayArrow } from '@mui/icons-material';
|
||||
import { BACK } from 'util/useBackTo';
|
||||
import { IChapter } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ResumeFABProps {
|
||||
chapter: IChapter;
|
||||
@@ -18,6 +19,8 @@ interface ResumeFABProps {
|
||||
}
|
||||
|
||||
export default function ResumeFab(props: ResumeFABProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
chapter: { index, lastPageRead },
|
||||
mangaId,
|
||||
@@ -34,7 +37,7 @@ export default function ResumeFab(props: ResumeFABProps) {
|
||||
}}
|
||||
>
|
||||
<PlayArrow />
|
||||
{index === 1 ? 'Start' : 'Resume'}
|
||||
{index === 1 ? t('global.button.start') : t('global.button.resume')}
|
||||
</Fab>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import { Fab, Menu } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { pluralize } from 'components/util/helpers';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { IChapterWithMeta } from 'components/manga/ChapterList';
|
||||
import SelectionFABActionItem from 'components/manga/SelectionFABActionItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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 { t } = useTranslation();
|
||||
|
||||
const { selectedChapters, onAction } = props;
|
||||
const count = selectedChapters.length;
|
||||
|
||||
@@ -44,7 +46,7 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||
ref={anchorEl}
|
||||
>
|
||||
<Fab variant="extended" color="primary" id="selectionMenuButton" onClick={() => setOpen(true)}>
|
||||
{`${count} ${pluralize(count, 'chapter')}`}
|
||||
{`${count} ${t('chapter.title', { count })}`}
|
||||
<MoreHoriz sx={{ ml: 1 }} />
|
||||
</Fab>
|
||||
<Menu
|
||||
@@ -64,37 +66,37 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||
({ chapter: c, downloadChapter: dc }) => !c.downloaded && dc === undefined,
|
||||
)}
|
||||
onClick={handleAction}
|
||||
title="Download selected"
|
||||
title={t('chapter.action.download.add.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="delete"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.downloaded)}
|
||||
onClick={handleAction}
|
||||
title="Delete selected"
|
||||
title={t('chapter.action.download.delete.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="bookmark"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.bookmarked)}
|
||||
onClick={handleAction}
|
||||
title="Bookmark selected"
|
||||
title={t('chapter.action.bookmark.add.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="unbookmark"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.bookmarked)}
|
||||
onClick={handleAction}
|
||||
title="Remove bookmarks from selected"
|
||||
title={t('chapter.action.bookmark.remove.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="mark_as_read"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.read)}
|
||||
onClick={handleAction}
|
||||
title="Mark selected as read"
|
||||
title={t('chapter.action.mark_as_read.add.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="mark_as_unread"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.read)}
|
||||
onClick={handleAction}
|
||||
title="Mark selected as unread"
|
||||
title={t('chapter.action.mark_as_read.remove.button.selected')}
|
||||
/>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
@@ -5,8 +5,16 @@
|
||||
* 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/. */
|
||||
|
||||
import { t } from 'i18next';
|
||||
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 = {
|
||||
active: false,
|
||||
@@ -35,7 +43,7 @@ function chapterOptionsReducer(state: ChapterListOptions, actions: ChapterOption
|
||||
case 'showChapterNumber':
|
||||
return { ...state, showChapterNumber: !state.showChapterNumber };
|
||||
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,
|
||||
);
|
||||
|
||||
export const SORT_OPTIONS: [ChapterSortMode, string][] = [
|
||||
['source', 'By Source'],
|
||||
['fetchedAt', 'By Fetch date'],
|
||||
export const SORT_OPTIONS: [ChapterSortMode, TranslationKey][] = [
|
||||
['source', 'global.sort.label.by_source'],
|
||||
['fetchedAt', 'global.sort.label.by_fetch_date'],
|
||||
];
|
||||
|
||||
export const isFilterActive = (options: ChapterListOptions) => {
|
||||
|
||||
@@ -10,40 +10,53 @@ import { CircularProgress } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Box } from '@mui/system';
|
||||
import React from 'react';
|
||||
import { IDownloadChapter } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IDownloadChapter, TranslationKey } from 'typings';
|
||||
|
||||
interface DownloadStateIndicatorProps {
|
||||
download: IDownloadChapter;
|
||||
}
|
||||
|
||||
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
width: '50px',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{download.progress !== 0 && <CircularProgress variant="determinate" value={download.progress * 100} />}
|
||||
// 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
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
width: '50px',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" component="div" color="text.secondary">
|
||||
{download.progress !== 0 && `${Math.round(download.progress * 100)}%`}
|
||||
{download.progress === 0 && download.state}
|
||||
</Typography>
|
||||
{download.progress !== 0 && <CircularProgress variant="determinate" value={download.progress * 100} />}
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" component="div" color="text.secondary">
|
||||
{download.progress !== 0 && `${Math.round(download.progress * 100)}%`}
|
||||
{download.progress === 0 && t(DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP[download.state])}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadStateIndicator;
|
||||
|
||||
@@ -31,56 +31,54 @@ import { createPortal } from 'react-dom';
|
||||
import useBackTo from 'util/useBackTo';
|
||||
import DesktopSideBar from 'components/navbar/navigation/DesktopSideBar';
|
||||
import MobileBottomBar from 'components/navbar/navigation/MobileBottomBar';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
import { t } from 'i18next';
|
||||
import { NavbarItem } from 'typings';
|
||||
|
||||
const navbarItems: Array<NavbarItem> = [
|
||||
{
|
||||
path: '/library',
|
||||
title: t('DefaultNavBar.navbarItems.Library'),
|
||||
title: 'library.title',
|
||||
SelectedIconComponent: CollectionsBookmarkIcon,
|
||||
IconComponent: CollectionsOutlinedBookmarkIcon,
|
||||
show: 'both',
|
||||
},
|
||||
{
|
||||
path: '/updates',
|
||||
title: 'Updates',
|
||||
title: 'updates.title',
|
||||
SelectedIconComponent: NewReleasesIcon,
|
||||
IconComponent: NewReleasesOutlinedIcon,
|
||||
show: 'both',
|
||||
},
|
||||
{
|
||||
path: '/extensions',
|
||||
title: 'Extensions',
|
||||
title: 'extension.title',
|
||||
SelectedIconComponent: ExtensionIcon,
|
||||
IconComponent: ExtensionOutlinedIcon,
|
||||
show: 'desktop',
|
||||
},
|
||||
{
|
||||
path: '/sources',
|
||||
title: 'Sources',
|
||||
title: 'source.title',
|
||||
SelectedIconComponent: ExploreIcon,
|
||||
IconComponent: ExploreOutlinedIcon,
|
||||
show: 'desktop',
|
||||
},
|
||||
{
|
||||
path: '/browse',
|
||||
title: 'Browse',
|
||||
title: 'global.label.browse',
|
||||
SelectedIconComponent: ExploreIcon,
|
||||
IconComponent: ExploreOutlinedIcon,
|
||||
show: 'mobile',
|
||||
},
|
||||
{
|
||||
path: '/downloads',
|
||||
title: 'Downloads',
|
||||
title: 'download.title',
|
||||
SelectedIconComponent: GetAppIcon,
|
||||
IconComponent: GetAppOutlinedIcon,
|
||||
show: 'both',
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
title: 'Settings',
|
||||
title: 'settings.title',
|
||||
SelectedIconComponent: SettingsIcon,
|
||||
IconComponent: SettingsIcon,
|
||||
show: 'both',
|
||||
@@ -88,7 +86,6 @@ const navbarItems: Array<NavbarItem> = [
|
||||
];
|
||||
|
||||
export default function DefaultNavBar() {
|
||||
// const { t } = useTranslation();
|
||||
const { title, action, override } = useContext(NavBarContext);
|
||||
const backTo = useBackTo();
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { styled } from '@mui/system';
|
||||
import useBackTo from 'util/useBackTo';
|
||||
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
|
||||
import { IChapter, IManga, IMangaCard, IReaderSettings } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
top: 0,
|
||||
@@ -121,6 +122,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export default function ReaderNavBar(props: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const backTo = useBackTo();
|
||||
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>
|
||||
<IconButton
|
||||
edge="start"
|
||||
@@ -258,7 +260,7 @@ export default function ReaderNavBar(props: IProps) {
|
||||
<Divider sx={{ my: 1, mx: 2 }} />
|
||||
<Navigation>
|
||||
<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}>
|
||||
<Select
|
||||
MenuProps={MenuProps}
|
||||
@@ -278,11 +280,11 @@ export default function ReaderNavBar(props: IProps) {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<span>{`of ${chapter.pageCount}`}</span>
|
||||
<span>{t('reader.page_info.label.of_max_pages', { maxPages: chapter.pageCount })}</span>
|
||||
</PageNavigation>
|
||||
<ChapterNavigation>
|
||||
<IconButton
|
||||
title="Previous Chapter"
|
||||
title={t('reader.button.previous_chapter')}
|
||||
sx={{ gridArea: 'pre' }}
|
||||
disabled={chapter.index <= 1}
|
||||
onClick={() => {
|
||||
@@ -317,14 +319,14 @@ export default function ReaderNavBar(props: IProps) {
|
||||
.map((ignoreValue, index) => (
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`Chapter ${
|
||||
index + 1
|
||||
}`}</MenuItem>
|
||||
<MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`${t(
|
||||
'chapter.title',
|
||||
)} ${index + 1}`}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
title="Next Chapter"
|
||||
title={t('reader.button.next_chapter')}
|
||||
sx={{ gridArea: 'next' }}
|
||||
disabled={chapter.index < 1 || chapter.index >= chapter.chapterCount}
|
||||
onClick={() => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import client, { useQuery } from 'util/client';
|
||||
import { ICategory } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
@@ -24,6 +25,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
export default function CategorySelect(props: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { open, setOpen, mangaId } = props;
|
||||
|
||||
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(`/api/v1/manga/${mangaId}/category`);
|
||||
@@ -65,14 +68,14 @@ export default function CategorySelect(props: IProps) {
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Set categories</DialogTitle>
|
||||
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{allCategories.length === 0 && (
|
||||
<span>
|
||||
No categories found!
|
||||
{t('category.error.no_categories_found.label.info')}
|
||||
<br />
|
||||
You should make some from settings.
|
||||
{t('category.error.no_categories_found.label.hint')}
|
||||
</span>
|
||||
)}
|
||||
{allCategories.map((category) => (
|
||||
@@ -92,10 +95,10 @@ export default function CategorySelect(props: IProps) {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { List, ListItemSecondaryAction, ListItemText } from '@mui/material';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import { langCodeToName } from 'util/language';
|
||||
import cloneObject from 'util/cloneObject';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function removeAll(firstList: any[], secondList: any[]) {
|
||||
secondList.forEach((item) => {
|
||||
@@ -38,6 +39,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
export default function LangSelect(props: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { shownLangs, setShownLangs, allLangs, forcedLangs } = props;
|
||||
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||
const [mShownLangs, setMShownLangs] = useState(removeAll(cloneObject(shownLangs), forcedLangs!));
|
||||
@@ -85,7 +88,7 @@ export default function LangSelect(props: IProps) {
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Enabled Languages</DialogTitle>
|
||||
<DialogTitle>{t('global.language.title.enabled_languages')}</DialogTitle>
|
||||
<DialogContent dividers sx={{ padding: 0 }}>
|
||||
<List>
|
||||
{allLangs.map((lang) => (
|
||||
@@ -104,10 +107,10 @@ export default function LangSelect(props: IProps) {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Link, useLocation } from 'react-router-dom';
|
||||
import { styled } from '@mui/system';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { NavbarItem } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const SideNavBarContainer = styled('div')(({ theme }) => ({
|
||||
height: '100vh',
|
||||
@@ -29,6 +30,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export default function DesktopSideBar({ navBarItems }: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -48,7 +50,7 @@ export default function DesktopSideBar({ navBarItems }: IProps) {
|
||||
<Link to={path} style={{ color: 'inherit', textDecoration: 'none' }} key={path}>
|
||||
<ListItem disableRipple button key={title}>
|
||||
<ListItemIcon sx={{ minWidth: '0' }}>
|
||||
<Tooltip placement="right" title={title}>
|
||||
<Tooltip placement="right" title={t(title)}>
|
||||
{iconFor(path, IconComponent, SelectedIconComponent)}
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { styled, Box } from '@mui/system';
|
||||
import { Link as RRDLink, useLocation } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { NavbarItem } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BottomNavContainer = styled('div')(({ theme }) => ({
|
||||
bottom: 0,
|
||||
@@ -38,6 +39,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export default function MobileBottomBar({ navBarItems }: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -68,7 +70,7 @@ export default function MobileBottomBar({ navBarItems }: IProps) {
|
||||
: 'grey.600',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
{t(title)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
@@ -12,6 +12,7 @@ import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React from 'react';
|
||||
import { IReaderSettings } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps extends IReaderSettings {
|
||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
||||
@@ -24,10 +25,12 @@ export default function ReaderSettingsOptions({
|
||||
showPageNumber,
|
||||
setSettingValue,
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="Static Navigation" />
|
||||
<ListItemText primary={t('reader.settings.label.static_navigation')} />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -37,7 +40,7 @@ export default function ReaderSettingsOptions({
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Show page number" />
|
||||
<ListItemText primary={t('reader.settings.label.show_page_number')} />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -47,7 +50,7 @@ export default function ReaderSettingsOptions({
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Load next chapter at ending" />
|
||||
<ListItemText primary={t('reader.settings.label.load_next_chapter')} />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -57,24 +60,30 @@ export default function ReaderSettingsOptions({
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Reader Type" />
|
||||
<ListItemText primary={t('reader.settings.label.reader_type')} />
|
||||
<Select
|
||||
variant="standard"
|
||||
value={readerType}
|
||||
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<MenuItem value="SingleLTR">Single Page (LTR)</MenuItem>
|
||||
<MenuItem value="SingleRTL">Single Page (RTL)</MenuItem>
|
||||
<MenuItem value="SingleLTR">{t('reader.settings.reader_type.label.single_page_ltr')}</MenuItem>
|
||||
<MenuItem value="SingleRTL">{t('reader.settings.reader_type.label.single_page_rtl')}</MenuItem>
|
||||
{/* <MenuItem value="SingleVertical">
|
||||
Vertical(WIP)
|
||||
</MenuItem> */}
|
||||
<MenuItem value="DoubleLTR">Double Page (LTR)</MenuItem>
|
||||
<MenuItem value="DoubleRTL">Double Page (RTL)</MenuItem>
|
||||
<MenuItem value="Webtoon">Webtoon</MenuItem>
|
||||
<MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
|
||||
<MenuItem value="ContinuesHorizontalLTR">Horizontal (LTR)</MenuItem>
|
||||
<MenuItem value="ContinuesHorizontalRTL">Horizontal (RTL)</MenuItem>
|
||||
<MenuItem value="DoubleLTR">{t('reader.settings.reader_type.label.double_page_ltr')}</MenuItem>
|
||||
<MenuItem value="DoubleRTL">{t('reader.settings.reader_type.label.double_page_rtl')}</MenuItem>
|
||||
<MenuItem value="Webtoon">{t('reader.settings.reader_type.label.webtoon')}</MenuItem>
|
||||
<MenuItem value="ContinuesVertical">
|
||||
{t('reader.settings.reader_type.label.continuous_vertical')}
|
||||
</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>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
@@ -9,9 +9,12 @@ import { IconButton, Menu, MenuItem, FormControlLabel, Radio } from '@mui/materi
|
||||
import React from 'react';
|
||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
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
|
||||
export default function SourceGridLayout() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
options: { SourcegridLayout },
|
||||
setOptions,
|
||||
@@ -53,7 +56,7 @@ export default function SourceGridLayout() {
|
||||
>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<FormControlLabel
|
||||
label="Compact grid"
|
||||
label={t('global.grid_layout.label.compact_grid')}
|
||||
value={GridLayout.Compact}
|
||||
control={
|
||||
<Radio
|
||||
@@ -66,7 +69,7 @@ export default function SourceGridLayout() {
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<FormControlLabel
|
||||
label="Comfortable grid"
|
||||
label={t('global.grid_layout.label.comfortable_grid')}
|
||||
control={
|
||||
<Radio
|
||||
name={GridLayout.Comfortable.toString()}
|
||||
@@ -78,7 +81,7 @@ export default function SourceGridLayout() {
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<FormControlLabel
|
||||
label="List"
|
||||
label={t('global.grid_layout.label.list')}
|
||||
control={
|
||||
<Radio
|
||||
name={GridLayout.List.toString()}
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
import React from 'react';
|
||||
import MangaGrid, { IMangaGridProps } from 'components/MangaGrid';
|
||||
import { IMangaCard } from 'typings';
|
||||
|
||||
const FILTERED_OUT_MESSAGE = 'There are no Manga matching this filter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
||||
return mangas;
|
||||
}
|
||||
|
||||
export default function SourceMangaGrid(props: IMangaGridProps) {
|
||||
const { t } = useTranslation();
|
||||
const { mangas, isLoading, hasNextPage, lastPageNum, setLastPageNum, message, messageExtra, gridLayout } = props;
|
||||
|
||||
const filteredManga = filterManga(mangas);
|
||||
@@ -29,7 +29,7 @@ export default function SourceMangaGrid(props: IMangaGridProps) {
|
||||
hasNextPage={hasNextPage}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={showFilteredOutMessage ? FILTERED_OUT_MESSAGE : message}
|
||||
message={showFilteredOutMessage ? t('manga.error.label.no_matches') : message}
|
||||
messageExtra={messageExtra}
|
||||
gridLayout={gridLayout}
|
||||
inLibraryIndicator
|
||||
|
||||
@@ -22,6 +22,7 @@ import TriStateFilter from 'components/source/filters/TriStateFilter';
|
||||
import GroupFilter from 'components/source/filters/GroupFilter';
|
||||
import SeperatorFilter from 'components/source/filters/SeparatorFilter';
|
||||
import { ISourceFilters, IState } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IFilters {
|
||||
sourceFilter: ISourceFilters[];
|
||||
@@ -143,6 +144,7 @@ export default function SourceOptions({
|
||||
setSearch,
|
||||
update,
|
||||
}: IFilters1) {
|
||||
const { t } = useTranslation();
|
||||
const [FilterOptions, setFilterOptions] = React.useState(false);
|
||||
|
||||
function handleReset() {
|
||||
@@ -165,14 +167,14 @@ export default function SourceOptions({
|
||||
color="primary"
|
||||
>
|
||||
<FilterListIcon />
|
||||
Filter
|
||||
{t('global.button.filter')}
|
||||
</Fab>
|
||||
|
||||
<OptionsPanel open={FilterOptions} onClose={() => setFilterOptions(false)}>
|
||||
<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}>
|
||||
Submit
|
||||
{t('global.button.submit')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
|
||||
@@ -16,8 +16,11 @@ import DialogActions from '@mui/material/DialogActions';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
import { EditTextPreferenceProps } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function EditTextPreference(props: EditTextPreferenceProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props;
|
||||
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
||||
@@ -57,10 +60,10 @@ export default function EditTextPreference(props: EditTextPreferenceProps) {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogCancel} color="primary">
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmit} color="primary">
|
||||
OK
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -17,6 +17,7 @@ import Radio from '@mui/material/Radio';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Button from '@mui/material/Button';
|
||||
import { ListPreferenceProps } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IListDialogProps {
|
||||
value: string;
|
||||
@@ -27,6 +28,8 @@ interface IListDialogProps {
|
||||
}
|
||||
|
||||
function ListDialog(props: IListDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { value: valueProp, open, onClose, options, title } = props;
|
||||
const [value, setValue] = React.useState(valueProp);
|
||||
const radioGroupRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -72,9 +75,9 @@ function ListDialog(props: IListDialogProps) {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel}>
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleOk}>Ok</Button>
|
||||
<Button onClick={handleOk}>{t('global.button.ok')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Button from '@mui/material/Button';
|
||||
import cloneObject from 'util/cloneObject';
|
||||
import { MultiSelectListPreferenceProps } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IListDialogProps {
|
||||
selectedValues: string[];
|
||||
@@ -28,6 +29,8 @@ interface IListDialogProps {
|
||||
}
|
||||
|
||||
function ListDialog(props: IListDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedValues: selectedValuesProp, open, onClose, values, title } = props;
|
||||
const [selectedValues, setSelectedValues] = React.useState(selectedValuesProp);
|
||||
|
||||
@@ -86,9 +89,9 @@ function ListDialog(props: IListDialogProps) {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel}>
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleOk}>Ok</Button>
|
||||
<Button onClick={handleOk}>{t('global.button.ok')}</Button>
|
||||
</DialogActions>
|
||||
</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());
|
||||
};
|
||||
Reference in New Issue
Block a user