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:
@@ -30,8 +30,8 @@ export default function Browse() {
|
||||
scrollButtons
|
||||
allowScrollButtonsMobile
|
||||
>
|
||||
<Tab sx={{ textTransform: 'none' }} label={t('screens.Browse.sources')} />
|
||||
<Tab sx={{ textTransform: 'none' }} label={t('screens.Browse.extensions')} />
|
||||
<Tab sx={{ textTransform: 'none' }} label={t('source.title')} />
|
||||
<Tab sx={{ textTransform: 'none' }} label={t('extension.title')} />
|
||||
</Tabs>
|
||||
<TabPanel index={0} currentIndex={tabNum}>
|
||||
<Sources />
|
||||
|
||||
@@ -51,7 +51,7 @@ const DownloadQueue: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(t('screens.DownloadQueue.download-queue'));
|
||||
setTitle(t('download.queue.title'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
@@ -59,7 +59,7 @@ const DownloadQueue: React.FC = () => {
|
||||
const onDragEnd = (result: DropResult) => {};
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useQueryParam, StringParam } from 'use-query-params';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { Typography, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { IExtension } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LANGUAGE = 0;
|
||||
const EXTENSIONS = 1;
|
||||
@@ -66,6 +67,8 @@ function groupExtensions(extensions: IExtension[]) {
|
||||
}
|
||||
|
||||
export default function MangaExtensions() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||
@@ -75,7 +78,7 @@ export default function MangaExtensions() {
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Extensions');
|
||||
setTitle(t('extension.title'));
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
@@ -120,18 +123,18 @@ export default function MangaExtensions() {
|
||||
inputRef.current.value = '';
|
||||
}
|
||||
|
||||
makeToast('Installing Extension File....', 'info');
|
||||
makeToast(t('extension.label.installing_file'), 'info');
|
||||
client
|
||||
.post('/api/v1/extension/install', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
.then(() => {
|
||||
makeToast('Installed extension successfully!', 'success');
|
||||
makeToast(t('extension.label.installed_successfully'), 'success');
|
||||
mutate();
|
||||
})
|
||||
.catch(() => makeToast('Extension installion failed!', 'error'));
|
||||
.catch(() => makeToast(t('extension.label.installation_failed'), 'error'));
|
||||
} 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);
|
||||
useEffect(() => {
|
||||
setTitle(t('screens.Library.Library'));
|
||||
setTitle(t('library.title'));
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
@@ -62,7 +62,7 @@ export default function Library() {
|
||||
if (tabsError != null) {
|
||||
return (
|
||||
<EmptyView
|
||||
message={t('screens.Library.could-not-load-categories')}
|
||||
message={t('category.error.label.request_failure')}
|
||||
messageExtra={tabsError?.message ?? tabsError}
|
||||
/>
|
||||
);
|
||||
@@ -73,7 +73,7 @@ export default function Library() {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -81,7 +81,7 @@ export default function Library() {
|
||||
<LibraryMangaGrid
|
||||
mangas={mangas}
|
||||
lastLibraryUpdate={lastLibraryUpdate}
|
||||
message={t('screens.Library.your-library-is-empty') as string}
|
||||
message={t('library.error.label.empty') as string}
|
||||
isLoading={activeTab != null && mangaLoading}
|
||||
/>
|
||||
);
|
||||
@@ -112,14 +112,14 @@ export default function Library() {
|
||||
{tab === activeTab &&
|
||||
(mangaError ? (
|
||||
<EmptyView
|
||||
message={t('screens.Library.could-not-load-manga')}
|
||||
message={t('manga.error.label.request_failure')}
|
||||
messageExtra={mangaError?.message ?? mangaError}
|
||||
/>
|
||||
) : (
|
||||
<LibraryMangaGrid
|
||||
mangas={mangas}
|
||||
lastLibraryUpdate={lastLibraryUpdate}
|
||||
message={t('screens.Library.category-is-empty') as string}
|
||||
message={t('library.error.label.empty') as string}
|
||||
isLoading={mangaLoading}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { NavbarToolbar } from 'components/navbar/DefaultNavBar';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from 'util/client';
|
||||
import { IManga } from 'typings';
|
||||
@@ -24,6 +25,8 @@ import { IManga } from 'typings';
|
||||
const AUTOFETCH_AGE = 60 * 60 * 24; // 24 hours
|
||||
|
||||
const Manga: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { setTitle } = useContext(NavbarContext);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const autofetchedRef = useRef(false);
|
||||
@@ -58,11 +61,11 @@ const Manga: React.FC = () => {
|
||||
}, [manga]);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(manga?.title ?? 'Manga');
|
||||
setTitle(manga?.title ?? t('manga.title'));
|
||||
}, [manga?.title]);
|
||||
|
||||
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 (
|
||||
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
||||
@@ -72,7 +75,7 @@ const Manga: React.FC = () => {
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Could not fetch manga data
|
||||
{t('manga.error.label.request_failure')}
|
||||
<br />
|
||||
{error.message ?? error}
|
||||
</>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from 'util/readerSettings';
|
||||
import makeToast from 'components/util/Toast';
|
||||
import { IChapter, IManga, IMangaCard, IPartialChapter, IReaderSettings, ReaderType } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getReaderComponent = (readerType: ReaderType) => {
|
||||
switch (readerType) {
|
||||
@@ -62,6 +63,7 @@ const initialChapter = () => ({
|
||||
});
|
||||
|
||||
export default function Reader() {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
@@ -87,13 +89,13 @@ export default function Reader() {
|
||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||
setSettings({ ...settings, [key]: value });
|
||||
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(() => {
|
||||
if (!manga?.title || (chapter as IChapter)?.name === 'Loading...') {
|
||||
setTitle(`Reader - Manga ${mangaId} Chapter ${chapterIndex}`);
|
||||
if (!manga?.title || (chapter as IChapter)?.name === t('global.label.loading')) {
|
||||
setTitle(t('reader.title'));
|
||||
} else {
|
||||
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 useLocalStorage from 'util/useLocalStorage';
|
||||
import { ISource } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function sourceToLangList(sources: ISource[]) {
|
||||
const result: string[] = [];
|
||||
@@ -34,6 +35,8 @@ function sourceToLangList(sources: ISource[]) {
|
||||
}
|
||||
|
||||
const SearchAll: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
||||
@@ -53,7 +56,7 @@ const SearchAll: React.FC = () => {
|
||||
const limit = new PQueue({ concurrency: 5 });
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Global Search');
|
||||
setTitle(t('search.title.global_search'));
|
||||
setAction(<AppbarSearch />);
|
||||
}, []);
|
||||
|
||||
@@ -146,7 +149,7 @@ const SearchAll: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Sources');
|
||||
setTitle(t('source.title'));
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch autoOpen />
|
||||
@@ -210,7 +213,7 @@ const SearchAll: React.FC = () => {
|
||||
setLastPageNum={setLastPageNum}
|
||||
horizontal
|
||||
noFaces
|
||||
message={fetched[id] ? 'No manga was found!' : undefined}
|
||||
message={fetched[id] ? t('manga.error.label.no_mangas_found') : undefined}
|
||||
inLibraryIndicator
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -36,11 +36,14 @@ import DarkTheme from 'components/context/DarkTheme';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import ListItemLink from 'components/util/ListItemLink';
|
||||
import SearchSettings from 'screens/settings/SearchSettings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => {
|
||||
setTitle('Settings');
|
||||
setTitle(t('settings.title'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
@@ -99,25 +102,25 @@ export default function Settings() {
|
||||
<ListItemIcon>
|
||||
<ListAltIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Categories" />
|
||||
<ListItemText primary={t('category.title.categories')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to="/settings/defaultReaderSettings">
|
||||
<ListItemIcon>
|
||||
<AutoStoriesIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Default Reader Settings" />
|
||||
<ListItemText primary={t('reader.settings.title.default_reader_settings')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to="/settings/backup">
|
||||
<ListItemIcon>
|
||||
<BackupIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Backup" />
|
||||
<ListItemText primary={t('settings.backup.title')} />
|
||||
</ListItemLink>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Brightness6Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dark Theme" />
|
||||
<ListItemText primary={t('settings.label.dark_theme')} />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch edge="end" checked={darkTheme} onChange={() => setDarkTheme(!darkTheme)} />
|
||||
</ListItemSecondaryAction>
|
||||
@@ -128,7 +131,7 @@ export default function Settings() {
|
||||
<ViewModuleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Manga Item width"
|
||||
primary={t('settings.label.manga_item_width')}
|
||||
secondary={`px:${ItemWidth}`}
|
||||
onClick={() => {
|
||||
handleDialogOpenItemWidth();
|
||||
@@ -139,7 +142,10 @@ export default function Settings() {
|
||||
<ListItemIcon>
|
||||
<FavoriteIcon />
|
||||
</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>
|
||||
<Switch edge="end" checked={showNsfw} onChange={() => setShowNsfw(!showNsfw)} />
|
||||
</ListItemSecondaryAction>
|
||||
@@ -149,9 +155,8 @@ export default function Settings() {
|
||||
<CachedIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Use image cache"
|
||||
secondary="Disabling image cache makes images load faster if you have a slow disk,
|
||||
but uses it much more internet traffic in turn"
|
||||
primary={t('settings.label.image_cache')}
|
||||
secondary={t('settings.label.image_cache_description')}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch edge="end" checked={useCache} onChange={() => setUseCache(!useCache)} />
|
||||
@@ -161,7 +166,7 @@ export default function Settings() {
|
||||
<ListItemIcon>
|
||||
<DnsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Server Address" secondary={serverAddress} />
|
||||
<ListItemText primary={t('settings.about.label.server_address')} secondary={serverAddress} />
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
@@ -177,18 +182,18 @@ export default function Settings() {
|
||||
<ListItemIcon>
|
||||
<InfoIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="About" />
|
||||
<ListItemText primary={t('settings.about.title')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||
<DialogContent>
|
||||
<DialogContentText>Enter Server Address</DialogContentText>
|
||||
<DialogContentText>{t('settings.server_address.dialog.label.enter_address')}</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Server Address"
|
||||
label={t('settings.about.label.server_address')}
|
||||
type="text"
|
||||
fullWidth
|
||||
value={dialogValue}
|
||||
@@ -198,16 +203,16 @@ export default function Settings() {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogCancel} color="primary">
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmit} color="primary">
|
||||
Set
|
||||
{t('global.button.set')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
|
||||
<DialogTitle>Manga Item width</DialogTitle>
|
||||
<DialogTitle>{t('settings.label.manga_item_width')}</DialogTitle>
|
||||
<DialogContent
|
||||
sx={{
|
||||
width: '98%',
|
||||
@@ -236,13 +241,13 @@ export default function Settings() {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogResetItemWidth} color="primary">
|
||||
Reset to Default
|
||||
{t('global.button.reset_to_default')}
|
||||
</Button>
|
||||
<Button onClick={handleDialogCancelItemWidth} color="primary">
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmitItemWidth} color="primary">
|
||||
OK
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -16,6 +16,7 @@ import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelec
|
||||
import List from '@mui/material/List';
|
||||
import cloneObject from 'util/cloneObject';
|
||||
import { SourcePreferences } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function getPrefComponent(type: string) {
|
||||
switch (type) {
|
||||
@@ -35,10 +36,11 @@ function getPrefComponent(type: string) {
|
||||
}
|
||||
|
||||
export default function SourceConfigure() {
|
||||
const { t } = useTranslation();
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Source Configuration');
|
||||
setTitle(t('source.configuration.title'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useQueryParam, StringParam } from 'use-query-params';
|
||||
import SourceGridLayout from 'components/source/GridLayouts';
|
||||
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||
import { IManga, IMangaCard, ISource, ISourceFilters } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IPos {
|
||||
position: number;
|
||||
@@ -26,6 +27,7 @@ interface IPos {
|
||||
}
|
||||
|
||||
export default function SourceMangas({ popular }: { popular: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
const history = useHistory();
|
||||
|
||||
@@ -58,7 +60,7 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
|
||||
}
|
||||
|
||||
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(() => {
|
||||
@@ -220,12 +222,14 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
|
||||
let messageExtra;
|
||||
|
||||
if (fetched) {
|
||||
message = 'No manga was found!';
|
||||
message = t('manga.error.label.no_mangas_found');
|
||||
if (sourceId === '0') {
|
||||
messageExtra = (
|
||||
<>
|
||||
<span>Check out </span>
|
||||
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">Local source guide</a>
|
||||
<span>{t('source.local_source.label.checkout')} </span>
|
||||
<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 { useQuery } from 'util/client';
|
||||
import { ISource } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function sourceToLangList(sources: ISource[]) {
|
||||
const result: string[] = [];
|
||||
@@ -44,6 +45,7 @@ function groupByLang(sources: ISource[]) {
|
||||
}
|
||||
|
||||
export default function Sources() {
|
||||
const { t } = useTranslation();
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||
@@ -70,7 +72,7 @@ export default function Sources() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Sources');
|
||||
setTitle(t('source.title'));
|
||||
setAction(
|
||||
<>
|
||||
<IconButton onClick={() => history.push('/sources/all/search/')} size="large">
|
||||
@@ -89,7 +91,7 @@ export default function Sources() {
|
||||
if (loading) return <LoadingPlaceholder />;
|
||||
|
||||
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 (
|
||||
|
||||
@@ -22,6 +22,8 @@ import { Link, useHistory } from 'react-router-dom';
|
||||
import client from 'util/client';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import { IChapter, IMangaChapter, IQueue, PaginatedList } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { t as translate } from 'i18next';
|
||||
|
||||
function epochToDate(epoch: number) {
|
||||
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) {
|
||||
const today = new Date();
|
||||
if (isTheSameDay(today, date)) return 'TODAY';
|
||||
if (isTheSameDay(today, date)) return translate('global.date.label.today');
|
||||
// calculate yesterday
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
if (isTheSameDay(yesterday, date)) return 'YESTERDAY';
|
||||
if (isTheSameDay(yesterday, date)) return translate('global.date.label.yesterday');
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
@@ -70,6 +72,7 @@ const initialQueue = {
|
||||
} as IQueue;
|
||||
|
||||
const Updates: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
@@ -97,7 +100,7 @@ const Updates: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Updates');
|
||||
setTitle(t('updates.title'));
|
||||
|
||||
setAction(null);
|
||||
}, []);
|
||||
@@ -136,7 +139,7 @@ const Updates: React.FC = () => {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
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) => {
|
||||
|
||||
@@ -7,12 +7,14 @@ import NavbarContext from 'components/context/NavbarContext';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
import { useQuery } from 'util/client';
|
||||
import { IAbout } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function About() {
|
||||
const { t } = useTranslation();
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('About');
|
||||
setTitle(t('settings.about.title'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
@@ -32,19 +34,22 @@ export default function About() {
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="Server" secondary={`${about.name} ${about.buildType}`} />
|
||||
<ListItemText
|
||||
primary={t('settings.about.label.server')}
|
||||
secondary={`${about.name} ${about.buildType}`}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Server version" secondary={version()} />
|
||||
<ListItemText primary={t('settings.about.label.server_version')} secondary={version()} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Build time" secondary={buildTime()} />
|
||||
<ListItemText primary={t('settings.about.label.build_time')} secondary={buildTime()} />
|
||||
</ListItem>
|
||||
<ListItemLink to={about.github}>
|
||||
<ListItemText primary="Github" secondary={about.github} />
|
||||
<ListItemText primary={t('settings.about.label.github')} secondary={about.github} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to={about.discord}>
|
||||
<ListItemText primary="Discord" secondary={about.discord} />
|
||||
<ListItemText primary={t('settings.about.label.discord')} secondary={about.discord} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
);
|
||||
|
||||
@@ -14,11 +14,13 @@ import client from 'util/client';
|
||||
import makeToast from 'components/util/Toast';
|
||||
import ListItemLink from 'components/util/ListItemLink';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function Backup() {
|
||||
const { t } = useTranslation();
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => {
|
||||
setTitle('Backup');
|
||||
setTitle(t('settings.backup.title'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
@@ -29,17 +31,17 @@ export default function Backup() {
|
||||
const formData = new FormData();
|
||||
formData.append('backup.proto.gz', file);
|
||||
|
||||
makeToast('Restoring backup....', 'info');
|
||||
makeToast(t('settings.backup.label.restoring_backup'), 'info');
|
||||
client
|
||||
.post('/api/v1/backup/import/file', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
.then(() => makeToast('Backup restore finished!', 'success'))
|
||||
.catch(() => makeToast('Backup restore failed!', 'error'));
|
||||
.then(() => makeToast(t('settings.backup.label.restored_backup'), 'success'))
|
||||
.catch(() => makeToast(t('settings.backup.label.backup_restore_failed'), 'error'));
|
||||
} else if (file.name.toLowerCase().endsWith('json')) {
|
||||
makeToast('legacy backups are not supported!', 'error');
|
||||
makeToast(t('settings.backup.label.legacy_backup_unsupported'), 'error');
|
||||
} 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 }}>
|
||||
<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>
|
||||
<ListItem button onClick={() => document.getElementById('backup-file')?.click()}>
|
||||
<ListItemText
|
||||
primary="Restore Backup"
|
||||
secondary="You can also drag and drop the backup file here to restore"
|
||||
primary={t('settings.backup.label.restore_backup')}
|
||||
secondary={t('settings.backup.label.restore_backup_info')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
@@ -34,6 +34,7 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
import client, { useQuery } from 'util/client';
|
||||
import { ICategory } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getItemStyle = (
|
||||
isDragging: boolean,
|
||||
@@ -49,9 +50,11 @@ const getItemStyle = (
|
||||
});
|
||||
|
||||
export default function Categories() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => {
|
||||
setTitle('Categories');
|
||||
setTitle(t('category.title.categories'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
@@ -196,14 +199,16 @@ export default function Categories() {
|
||||
</Fab>
|
||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||
<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>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Category Name"
|
||||
label={t('category.label.category_name')}
|
||||
type="text"
|
||||
fullWidth
|
||||
value={dialogName}
|
||||
@@ -217,15 +222,15 @@ export default function Categories() {
|
||||
color="default"
|
||||
/>
|
||||
}
|
||||
label="Default category when adding new manga to library"
|
||||
label={t('category.label.use_as_default_category')}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogCancel} color="primary">
|
||||
Cancel
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmit} color="primary">
|
||||
Submit
|
||||
{t('global.button.submit')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
} from 'util/readerSettings';
|
||||
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
|
||||
import { IReaderSettings } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function DefaultReaderSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => {
|
||||
setTitle('Default Reader Settings');
|
||||
setTitle(t('reader.settings.title.default_reader_settings'));
|
||||
setAction(null);
|
||||
}, []);
|
||||
|
||||
@@ -33,7 +35,7 @@ export default function DefaultReaderSettings() {
|
||||
|
||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||
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 SearchIcon from '@mui/icons-material/Search';
|
||||
import { SearchMetadataKeys } from 'typings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SearchSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { metadata, settings } = useSearchSettings();
|
||||
|
||||
const setSettingValue = (key: SearchMetadataKeys, value: boolean) => {
|
||||
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 (
|
||||
@@ -21,7 +23,7 @@ export default function SearchSettings() {
|
||||
<ListItemIcon>
|
||||
<SearchIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Ignore Filters when Searching" />
|
||||
<ListItemText primary={t('search.label.ignore_filters')} />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
|
||||
Reference in New Issue
Block a user