diff --git a/src/components/ExtensionCard.tsx b/src/components/ExtensionCard.tsx index 9a4417b3..daa214c5 100644 --- a/src/components/ExtensionCard.tsx +++ b/src/components/ExtensionCard.tsx @@ -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('serverBaseURL', ''); const [useCache] = useLocalStorage('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 => { 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])} diff --git a/src/components/MangaCard.tsx b/src/components/MangaCard.tsx index 0a061586..c82aa764 100644 --- a/src/components/MangaCard.tsx +++ b/src/components/MangaCard.tsx @@ -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((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((props: IProps, ref) > {inLibraryIndicator && inLibrary && ( - In library + {t('manga.button.in_library')} )} {showUnreadBadge && unread! > 0 && ( @@ -252,7 +255,9 @@ const MangaCard = React.forwardRef((props: IProps, ref) {inLibraryIndicator && inLibrary && ( - In library + + {t('manga.button.in_library')} + )} {showUnreadBadge && unread! > 0 && ( {unread} diff --git a/src/components/SourceCard.tsx b/src/components/SourceCard.tsx index 36f53e60..b690cd4a 100644 --- a/src/components/SourceCard.tsx +++ b/src/components/SourceCard.tsx @@ -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 = (props: IProps) => { + const { t } = useTranslation(); + const { source: { id, name, lang, iconUrl, supportsLatest, isNsfw }, } = props; @@ -110,18 +113,18 @@ const SourceCard: React.FC = (props: IProps) => { {supportsLatest && ( )} {supportsLatest && ( )} diff --git a/src/components/library/LibraryMangaGrid.tsx b/src/components/library/LibraryMangaGrid.tsx index daa58bb7..f315d809 100644 --- a/src/components/library/LibraryMangaGrid.tsx +++ b/src/components/library/LibraryMangaGrid.tsx @@ -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, { unreadCount }: IMangaCard): boolean => { switch (unread) { @@ -118,6 +117,8 @@ const LibraryMangaGrid: React.FC { + const { t } = useTranslation(); + const [query] = useQueryParam('query', StringParam); const { options } = useLibraryOptionsContext(); const { unread, downloaded } = options; @@ -155,7 +156,7 @@ const LibraryMangaGrid: React.FC ); diff --git a/src/components/library/LibraryOptionsPanel.tsx b/src/components/library/LibraryOptionsPanel.tsx index d3e206ba..1433af78 100644 --- a/src/components/library/LibraryOptionsPanel.tsx +++ b/src/components/library/LibraryOptionsPanel.tsx @@ -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 = ({ open, onClose }) => { + const { t } = useTranslation(); const { options, setOptions } = useLibraryOptionsContext(); const handleFilterChange = (key: T, value: LibraryOptions[T]) => { @@ -45,18 +47,18 @@ const LibraryOptionsPanel: React.FC = ({ 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 ( <> handleFilterChange('unread', c)} /> handleFilterChange('downloaded', c)} /> @@ -67,7 +69,7 @@ const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { return SORT_OPTIONS.map(([mode, label]) => ( @@ -82,36 +84,36 @@ const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { const { gridLayout, showDownloadBadge, showUnreadBadge } = options; return ( <> - Display mode + {t('global.grid_layout.title')} handleFilterChange('gridLayout', Number(e.target.value))} value={gridLayout} > - Badges + {t('library.option.display.badge.title')} handleFilterChange('showUnreadBadge', !showUnreadBadge)} /> handleFilterChange('showDownloadBadge', !showDownloadBadge)} /> diff --git a/src/components/library/UpdateChecker.tsx b/src/components/library/UpdateChecker.tsx index 6f82d120..2edd1629 100644 --- a/src/components/library/UpdateChecker.tsx +++ b/src/components/library/UpdateChecker.tsx @@ -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); } }; diff --git a/src/components/manga/ChapterCard.tsx b/src/components/manga/ChapterCard.tsx index eb33413d..125be776 100644 --- a/src/components/manga/ChapterCard.tsx +++ b/src/components/manga/ChapterCard.tsx @@ -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 = (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 = (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} {chapter.scanlator} {getUploadDateString(chapter.uploadDate)} - {isDownloaded && ' • Downloaded'} + {isDownloaded && ` • ${t('chapter.status.label.downloaded')}`} @@ -160,14 +162,14 @@ const ChapterCard: React.FC = (props: IProps) => { - Select + {t('chapter.action.label.select')} {isDownloaded && ( - Delete + {t('chapter.action.download.delete.label.action')} )} {canBeDownloaded && ( @@ -175,7 +177,7 @@ const ChapterCard: React.FC = (props: IProps) => { - Download + {t('chapter.action.download.add.label.action')} )} sendChange('bookmarked', !chapter.bookmarked)}> @@ -184,8 +186,8 @@ const ChapterCard: React.FC = (props: IProps) => { {!chapter.bookmarked && } - {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')} sendChange('read', !chapter.read)}> @@ -194,15 +196,15 @@ const ChapterCard: React.FC = (props: IProps) => { {!chapter.read && } - {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')} sendChange('markPrevRead', true)}> - Mark previous as Read + {t('chapter.action.mark_as_read.add.label.action.previous')} diff --git a/src/components/manga/ChapterList.tsx b/src/components/manga/ChapterList.tsx index 4941ae7f..6e9e2e58 100644 --- a/src/components/manga/ChapterList.tsx +++ b/src/components/manga/ChapterList.tsx @@ -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 = ({ mangaId }) => { + const { t } = useTranslation(); + const [selection, setSelection] = useState(null); const prevQueueRef = useRef(); const queue = useSubscription('/api/v1/downloads').data?.queue; @@ -162,9 +169,9 @@ const ChapterList: React.FC = ({ 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 = ({ mangaId }) => { }} > - {`${visibleChapters.length} Chapter${visibleChapters.length === 1 ? '' : 's'}`} + {`${visibleChapters.length} ${t('chapter.title', { + count: visibleChapters.length, + })}`} {selection === null ? ( @@ -222,17 +231,17 @@ const ChapterList: React.FC = ({ mangaId }) => { ) : ( )} - {noChaptersFound && } - {noChaptersMatchingFilter && } + {noChaptersFound && } + {noChaptersMatchingFilter && } ; } -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 = ({ open, onClose, options, optionsDispatch }) => ( - - open={open} - onClose={onClose} - minHeight={150} - tabs={['filter', 'sort', 'display']} - tabTitle={(key) => TITLES[key]} - tabContent={(key) => { - if (key === 'filter') { - return ( - <> - - optionsDispatch({ - type: 'filter', - filterType: 'unread', - filterValue: c, - }) +const ChapterOptions: React.FC = ({ open, onClose, options, optionsDispatch }) => { + const { t } = useTranslation(); + + return ( + + open={open} + onClose={onClose} + minHeight={150} + tabs={['filter', 'sort', 'display']} + tabTitle={(key) => t(TITLES[key])} + tabContent={(key) => { + if (key === 'filter') { + return ( + <> + + optionsDispatch({ + type: 'filter', + filterType: 'unread', + filterValue: c, + }) + } + /> + + optionsDispatch({ + type: 'filter', + filterType: 'downloaded', + filterValue: c, + }) + } + /> + + optionsDispatch({ + type: 'filter', + filterType: 'bookmarked', + filterValue: c, + }) + } + /> + + ); + } + if (key === 'sort') { + return SORT_OPTIONS.map(([mode, label]) => ( + + mode !== options.sortBy + ? optionsDispatch({ type: 'sortBy', sortBy: mode }) + : optionsDispatch({ type: 'sortReverse' }) } /> - - optionsDispatch({ - type: 'filter', - filterType: 'downloaded', - filterValue: c, - }) - } - /> - - optionsDispatch({ - type: 'filter', - filterType: 'bookmarked', - filterValue: c, - }) - } - /> - - ); - } - if (key === 'sort') { - return SORT_OPTIONS.map(([mode, label]) => ( - - mode !== options.sortBy - ? optionsDispatch({ type: 'sortBy', sortBy: mode }) - : optionsDispatch({ type: 'sortReverse' }) - } - /> - )); - } - if (key === 'display') { - return ( - optionsDispatch({ type: 'showChapterNumber' })} - value={options.showChapterNumber} - > - - - - ); - } - return null; - }} - /> -); + )); + } + if (key === 'display') { + return ( + optionsDispatch({ type: 'showChapterNumber' })} + value={options.showChapterNumber} + > + + + + ); + } + return null; + }} + /> + ); +}; export default ChapterOptions; diff --git a/src/components/manga/MangaDetails.tsx b/src/components/manga/MangaDetails.tsx index 2d3474da..ca23ff02 100644 --- a/src/components/manga/MangaDetails.tsx +++ b/src/components/manga/MangaDetails.tsx @@ -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 = ({ manga }) => { + const { t } = useTranslation(); + const [serverAddress] = useLocalStorage('serverBaseURL', ''); const [useCache] = useLocalStorage('useCache', true); @@ -158,15 +161,15 @@ const MangaDetails: React.FC = ({ manga }) => {

{manga.title}

- {'Author: '} + {`${t('manga.label.author')}: `} {getValueOrUnknown(manga.author)}

- {'Artist: '} + {`${t('manga.label.artist')}: `} {getValueOrUnknown(manga.artist)}

-

{`Status: ${manga.status}`}

-

{`Source: ${getSourceName(manga.source)}`}

+

{`${t('manga.label.status')}: ${manga.status}`}

+

{`${t('source.title')}: ${getSourceName(manga.source)}`}

@@ -174,21 +177,23 @@ const MangaDetails: React.FC = ({ manga }) => { {manga.inLibrary ? : } - {manga.inLibrary ? 'In Library' : 'Add To Library'} + {manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
- Open Site + + {t('global.button.open_site')} +
-

About

+

{t('settings.about.title')}

{manga.description}

diff --git a/src/components/manga/MangaToolbarMenu.tsx b/src/components/manga/MangaToolbarMenu.tsx index 608ee1f6..ffdd2195 100644 --- a/src/components/manga/MangaToolbarMenu.tsx +++ b/src/components/manga/MangaToolbarMenu.tsx @@ -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 && ( <> - + { onRefresh(); @@ -55,7 +57,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { {manga.inLibrary && ( - + { setEditCategories(true); @@ -97,7 +99,7 @@ const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { - Reload data from source + {t('manga.label.reload_from_source')} {manga.inLibrary && ( { - Edit manga categories + {t('manga.label.edit_categories')} )} diff --git a/src/components/manga/ResumeFAB.tsx b/src/components/manga/ResumeFAB.tsx index 75899e06..d1fa6300 100644 --- a/src/components/manga/ResumeFAB.tsx +++ b/src/components/manga/ResumeFAB.tsx @@ -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) { }} > - {index === 1 ? 'Start' : 'Resume'} + {index === 1 ? t('global.button.start') : t('global.button.resume')} ); } diff --git a/src/components/manga/SelectionFAB.tsx b/src/components/manga/SelectionFAB.tsx index 2d7655db..ae8048e2 100644 --- a/src/components/manga/SelectionFAB.tsx +++ b/src/components/manga/SelectionFAB.tsx @@ -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 = (props) => { + const { t } = useTranslation(); + const { selectedChapters, onAction } = props; const count = selectedChapters.length; @@ -44,7 +46,7 @@ const SelectionFAB: React.FC = (props) => { ref={anchorEl} > setOpen(true)}> - {`${count} ${pluralize(count, 'chapter')}`} + {`${count} ${t('chapter.title', { count })}`} = (props) => { ({ chapter: c, downloadChapter: dc }) => !c.downloaded && dc === undefined, )} onClick={handleAction} - title="Download selected" + title={t('chapter.action.download.add.button.selected')} /> chapter.downloaded)} onClick={handleAction} - title="Delete selected" + title={t('chapter.action.download.delete.button.selected')} /> !chapter.bookmarked)} onClick={handleAction} - title="Bookmark selected" + title={t('chapter.action.bookmark.add.button.selected')} /> chapter.bookmarked)} onClick={handleAction} - title="Remove bookmarks from selected" + title={t('chapter.action.bookmark.remove.button.selected')} /> !chapter.read)} onClick={handleAction} - title="Mark selected as read" + title={t('chapter.action.mark_as_read.add.button.selected')} /> chapter.read)} onClick={handleAction} - title="Mark selected as unread" + title={t('chapter.action.mark_as_read.remove.button.selected')} /> diff --git a/src/components/manga/util.tsx b/src/components/manga/util.tsx index d1ee8f56..6c68726e 100644 --- a/src/components/manga/util.tsx +++ b/src/components/manga/util.tsx @@ -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) => { diff --git a/src/components/molecules/DownloadStateIndicator.tsx b/src/components/molecules/DownloadStateIndicator.tsx index 082df821..4231cfbf 100644 --- a/src/components/molecules/DownloadStateIndicator.tsx +++ b/src/components/molecules/DownloadStateIndicator.tsx @@ -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 = ({ download }) => ( - - {download.progress !== 0 && } +// 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 = ({ download }) => { + const { t } = useTranslation(); + + return ( - - {download.progress !== 0 && `${Math.round(download.progress * 100)}%`} - {download.progress === 0 && download.state} - + {download.progress !== 0 && } + + + {download.progress !== 0 && `${Math.round(download.progress * 100)}%`} + {download.progress === 0 && t(DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP[download.state])} + + - -); + ); +}; export default DownloadStateIndicator; diff --git a/src/components/navbar/DefaultNavBar.tsx b/src/components/navbar/DefaultNavBar.tsx index 61a82ed2..1ab9c432 100644 --- a/src/components/navbar/DefaultNavBar.tsx +++ b/src/components/navbar/DefaultNavBar.tsx @@ -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 = [ { 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 = [ ]; export default function DefaultNavBar() { - // const { t } = useTranslation(); const { title, action, override } = useContext(NavBarContext); const backTo = useBackTo(); diff --git a/src/components/navbar/ReaderNavBar.tsx b/src/components/navbar/ReaderNavBar.tsx index 3e4705d7..44976b66 100644 --- a/src/components/navbar/ReaderNavBar.tsx +++ b/src/components/navbar/ReaderNavBar.tsx @@ -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) { }, }} > - + - Currently on page + {t('reader.page_info.label.currently_on_page')} = chapter.chapterCount} onClick={() => { diff --git a/src/components/navbar/action/CategorySelect.tsx b/src/components/navbar/action/CategorySelect.tsx index 69f13265..2b541258 100644 --- a/src/components/navbar/action/CategorySelect.tsx +++ b/src/components/navbar/action/CategorySelect.tsx @@ -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(`/api/v1/manga/${mangaId}/category`); @@ -65,14 +68,14 @@ export default function CategorySelect(props: IProps) { maxWidth="xs" open={open} > - Set categories + {t('category.title.set_categories')} {allCategories.length === 0 && ( - No categories found! + {t('category.error.no_categories_found.label.info')}
- You should make some from settings. + {t('category.error.no_categories_found.label.hint')}
)} {allCategories.map((category) => ( @@ -92,10 +95,10 @@ export default function CategorySelect(props: IProps) {
diff --git a/src/components/navbar/action/LangSelect.tsx b/src/components/navbar/action/LangSelect.tsx index fd8dd864..7b5b8176 100644 --- a/src/components/navbar/action/LangSelect.tsx +++ b/src/components/navbar/action/LangSelect.tsx @@ -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} > - Enabled Languages + {t('global.language.title.enabled_languages')} {allLangs.map((lang) => ( @@ -104,10 +107,10 @@ export default function LangSelect(props: IProps) { diff --git a/src/components/navbar/navigation/DesktopSideBar.tsx b/src/components/navbar/navigation/DesktopSideBar.tsx index e9ad004a..0628272c 100644 --- a/src/components/navbar/navigation/DesktopSideBar.tsx +++ b/src/components/navbar/navigation/DesktopSideBar.tsx @@ -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) { - + {iconFor(path, IconComponent, SelectedIconComponent)} diff --git a/src/components/navbar/navigation/MobileBottomBar.tsx b/src/components/navbar/navigation/MobileBottomBar.tsx index d6b5bbd9..758c8276 100644 --- a/src/components/navbar/navigation/MobileBottomBar.tsx +++ b/src/components/navbar/navigation/MobileBottomBar.tsx @@ -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)} diff --git a/src/components/reader/ReaderSettingsOptions.tsx b/src/components/reader/ReaderSettingsOptions.tsx index 2c242498..1aa5c408 100644 --- a/src/components/reader/ReaderSettingsOptions.tsx +++ b/src/components/reader/ReaderSettingsOptions.tsx @@ -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 ( - + - + - + - + diff --git a/src/components/source/GridLayouts.tsx b/src/components/source/GridLayouts.tsx index 2107c5cf..cccf3b97 100644 --- a/src/components/source/GridLayouts.tsx +++ b/src/components/source/GridLayouts.tsx @@ -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() { > - Filter + {t('global.button.filter')} setFilterOptions(false)}> - + (currentValue); @@ -57,10 +60,10 @@ export default function EditTextPreference(props: EditTextPreferenceProps) { diff --git a/src/components/sourceConfiguration/ListPreference.tsx b/src/components/sourceConfiguration/ListPreference.tsx index 847c4dd9..e0ad2402 100644 --- a/src/components/sourceConfiguration/ListPreference.tsx +++ b/src/components/sourceConfiguration/ListPreference.tsx @@ -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(null); @@ -72,9 +75,9 @@ function ListDialog(props: IListDialogProps) { - + ); diff --git a/src/components/sourceConfiguration/MultiSelectListPreference.tsx b/src/components/sourceConfiguration/MultiSelectListPreference.tsx index 18f97fd8..d129c068 100644 --- a/src/components/sourceConfiguration/MultiSelectListPreference.tsx +++ b/src/components/sourceConfiguration/MultiSelectListPreference.tsx @@ -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) { - + ); diff --git a/src/components/util/helpers.ts b/src/components/util/helpers.ts deleted file mode 100644 index 65c2c7cc..00000000 --- a/src/components/util/helpers.ts +++ /dev/null @@ -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()); -}; diff --git a/src/i18n/i18next.d.ts b/src/i18n/i18next.d.ts new file mode 100644 index 00000000..d64f1282 --- /dev/null +++ b/src/i18n/i18next.d.ts @@ -0,0 +1,9 @@ +import 'i18next'; +import resources from 'i18n/translations'; + +declare module 'i18next' { + interface CustomTypeOptions { + returnNull: false; + resources: typeof resources.en; + } +} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index b45a3601..178086d5 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1,19 +1,426 @@ { - "screens": { - "Library": { - "Library": "Library", - "could-not-load-categories": "Could not load categories", - "your-library-is-empty": "Your Library is empty", - "category-is-empty": "Category is Empty", - "could-not-load-manga": "Could not load manga" + "category": { + "dialog": { + "title": { + "edit_category": "Edit Catalog", + "new_category": "New Catalog" + } }, - "Browse": { - "sources": "Sources", - "extensions": "Extensions" + "error": { + "label": { + "empty": "Category is Empty", + "request_failure": "Could not load categories" + }, + "no_categories_found": { + "label": { + "hint": "You should make some from settings.", + "info": "No categories found!" + } + } }, - "DownloadQueue": { - "no-downloads": "No downloads", - "download-queue": "Download Queue" + "label": { + "category_name": "Category Name", + "use_as_default_category": "Default category when adding new manga to library" + }, + "title": { + "categories": "Categories", + "set_categories": "Set categories" } + }, + "chapter": { + "action": { + "bookmark": { + "add": { + "button": { + "selected": "Bookmark selected" + }, + "label": { + "action": "Add bookmark", + "error_one": "Error bookmarking chapter", + "error_other": "Error bookmarking chapters", + "success_one": "Chapter bookmarked", + "success_other": "{{count}} chapters bookmarked" + } + }, + "remove": { + "button": { + "selected": "Remove bookmarks from selected" + }, + "label": { + "action": "Remove bookmark", + "error_one": "Error removing bookmark", + "error_other": "Error removing bookmarks", + "success_one": "Chapter bookmark removed", + "success_other": "{{count}} chapter bookmarks removed" + } + } + }, + "download": { + "add": { + "button": { + "selected": "Download selected" + }, + "label": { + "action": "Download", + "error_one": "Error adding download", + "error_other": "Error adding downloads", + "success_one": "Download added", + "success_other": "{{count}} downloads added" + } + }, + "delete": { + "button": { + "selected": "Delete selected" + }, + "label": { + "action": "Delete", + "error_one": "Error deleting chapter", + "error_other": "Error deleting chapters", + "success_one": "Chapter deleted", + "success_other": "{{count}} chapters deleted" + } + } + }, + "label": { + "select": "Select" + }, + "mark_as_read": { + "add": { + "button": { + "selected": "Mark selected as read" + }, + "label": { + "action": { + "current": "Mark as read", + "previous": "Mark previous as Read" + }, + "error_one": "Error marking chapter as read", + "error_other": "Error marking chapters as read", + "success_one": "Chapter marked as read", + "success_other": "{{count}} chapters marked as read" + } + }, + "remove": { + "button": { + "selected": "Mark selected as unread" + }, + "label": { + "action": "Mark as unread", + "error_one": "Error marking chapter as unread", + "error_other": "Error marking chapters as unread", + "success_one": "Chapter marked as unread", + "success_other": "{{count}} chapters marked as unread" + } + } + } + }, + "error": { + "label": { + "no_chapter_found": "No chapters found", + "no_matches": "No chapters matching filter" + } + }, + "option": { + "display": { + "label": { + "chapter_number": "Chapter Number", + "source_title": "Source Title" + } + } + }, + "status": { + "label": { + "downloaded": "Downloaded" + } + }, + "title": "Chapter", + "title_one": "Chapter", + "title_other": "Chapters" + }, + "download": { + "queue": { + "label": { + "no_downloads": "No downloads" + }, + "title": "Download Queue" + }, + "state": { + "label": { + "downloading": "Downloading", + "error": "Error", + "finished": "Finished", + "queued": "Queued" + } + }, + "title": "Downloads" + }, + "extension": { + "action": { + "label": { + "install": "install", + "uninstall": "uninstall", + "update": "update" + } + }, + "label": { + "installation_failed": "Extension installation failed!", + "installed_successfully": "Installed extension successfully!", + "installing_file": "Installing Extension File..." + }, + "language": { + "all": "All" + }, + "state": { + "label": { + "installing": "installing", + "obsolete": "obsolete", + "uninstalling": "uninstalling", + "updating": "updating" + } + }, + "title": "Extensions" + }, + "global": { + "button": { + "browse": "Browse", + "cancel": "Cancel", + "clear": "Clear", + "filter": "Filter", + "latest": "Latest", + "ok": "Ok", + "open_site": "Open Site", + "reset": "Reset", + "reset_to_default": "Reset to Default", + "resume": "Resume", + "select_all": "Select all", + "set": "Set", + "start": "Start", + "submit": "Submit" + }, + "date": { + "label": { + "today": "TODAY", + "today_at": "Today at {{timeString}}", + "yesterday": "YESTERDAY", + "yesterday_at": "Yesterday at {{timeString}}" + } + }, + "error": { + "label": { + "invalid_action": "This is not a valid Action", + "invalid_file_type": "invalid file type!", + "update_failed": "Checking for updates failed!" + } + }, + "filter": { + "label": { + "bookmarked": "Bookmarked", + "downloaded": "Downloaded", + "unread": "Unread" + } + }, + "grid_layout": { + "label": { + "comfortable_grid": "Comfortable grid", + "compact_grid": "Compact grid", + "list": "List" + }, + "title": "Display mode" + }, + "label": { + "browse": "Browse", + "display": "Display", + "filter": "Filter", + "loading": "Loading...", + "sort": "Sort" + }, + "language": { + "label": { + "language_with_code": "language with code: {{code}}" + }, + "title": { + "enabled_languages": "Enabled Languages" + } + }, + "sort": { + "label": { + "by_fetch_date": "By Fetch date", + "by_source": "By Source" + } + } + }, + "library": { + "error": { + "label": { + "empty": "Your Library is empty", + "no_matches": "There are no Manga matching this filter" + } + }, + "option": { + "display": { + "badge": { + "label": { + "download_badges": "Download Badges", + "unread_badges": "Unread Badges" + }, + "title": "Badges" + } + }, + "sort": { + "label": { + "alphabetically": "Alphabetically", + "by_date_added": "By Date Added", + "by_last_read": "By Last Read", + "by_unread_chapters": "By Unread chapters" + } + } + }, + "title": "Library" + }, + "manga": { + "button": { + "add_to_library": "Add To Library", + "in_library": "In Library" + }, + "error": { + "label": { + "no_mangas_found": "No manga found!", + "no_matches": "There are no Manga matching this filter", + "request_failure": "Could not load manga" + } + }, + "label": { + "artist": "Artist", + "author": "Author", + "edit_categories": "Edit manga categories", + "reload_from_source": "Reload data from source", + "status": "Status" + }, + "title": "Manga" + }, + "reader": { + "button": { + "next_chapter": "Next Chapter", + "previous_chapter": "Previous Chapter" + }, + "page_info": { + "label": { + "currently_on_page": "Currently on page", + "of_max_pages": "of {{maxPages}}" + } + }, + "settings": { + "error": { + "label": { + "failed_to_save_default_settings": "Failed to save the default reader settings to the server", + "failed_to_save_settings": "Failed to save the reader settings to the server" + } + }, + "label": { + "load_next_chapter": "Load next chapter at ending", + "reader_type": "Reader Type", + "show_page_number": "Show page number", + "static_navigation": "Static Navigation" + }, + "reader_type": { + "label": { + "continuous_horizontal_ltr": "Horizontal (LTR)", + "continuous_horizontal_rtl": "Horizontal (RTL)", + "continuous_vertical": "Continues Vertical", + "double_page_ltr": "Double Page (LTR)", + "double_page_rtl": "Double Page (RTL)", + "single_page_ltr": "Single Page (LTR)", + "single_page_rtl": "Single Page (RTL)", + "webtoon": "Webtoon" + } + }, + "title": { + "default_reader_settings": "Default Reader Settings", + "reader_settings": "Reader Settings" + } + }, + "title": "Reader - Manga {{mangaId}} Chapter {{chapterIndex}}" + }, + "search": { + "error": { + "label": { + "failed_to_save_settings": "Failed to save the default search settings to the server" + } + }, + "label": { + "ignore_filters": "Ignore Filters when Searching" + }, + "title": { + "global_search": "Global Search" + } + }, + "settings": { + "about": { + "label": { + "build_time": "Build time", + "discord": "Discord", + "github": "Github", + "server": "Server", + "server_address": "Server Address", + "server_version": "Server version" + }, + "title": "About" + }, + "backup": { + "label": { + "backup_restore_failed": "Backup restore failed!", + "create_backup": "Create Backup", + "create_backup_info": "Backup library as a Tachiyomi backup", + "legacy_backup_unsupported": "legacy backups are not supported!", + "restore_backup": "Restore Backup", + "restore_backup_info": "You can also drag and drop the backup file here to restore", + "restored_backup": "Backup restore finished!", + "restoring_backup": "Restoring backup..." + }, + "title": "Backup" + }, + "label": { + "dark_theme": "Dark Theme", + "image_cache": "Use image cache", + "image_cache_description": "Disabling image cache makes images load faster if you have a slow disk, but uses it much more internet traffic in turn", + "manga_item_width": "Manga Item width", + "show_nsfw": "Show NSFW", + "show_nsfw_description": "Hide NSFW extensions and sources" + }, + "server_address": { + "dialog": { + "label": { + "enter_address": "Enter Server Address" + } + } + }, + "title": "Settings" + }, + "source": { + "configuration": { + "title": "Source Configuration" + }, + "error": { + "label": { + "no_sources_found": "No sources found. Install Some Extensions first." + } + }, + "local_source": { + "label": { + "checkout": "Check out", + "guide": "Local source guide" + } + }, + "title": "Source", + "title_one": "Source", + "title_other": "Sources" + }, + "updates": { + "error": { + "label": { + "no_updates_available": "You don't have any updates yet." + } + }, + "title": "Updates" } } diff --git a/src/i18n/resources/ar.json b/src/i18n/resources/ar.json index e8113e9b..d220a37a 100644 --- a/src/i18n/resources/ar.json +++ b/src/i18n/resources/ar.json @@ -1,7 +1,5 @@ { - "screens": { - "Library": { - "Library": "المكتبة" - } + "library": { + "title": "المكتبة" } } diff --git a/src/i18n/resources/de.json b/src/i18n/resources/de.json index 8a2121ef..0ac712b1 100644 --- a/src/i18n/resources/de.json +++ b/src/i18n/resources/de.json @@ -1,19 +1,39 @@ { - "screens": { - "Library": { - "Library": "Bibliotek", - "could-not-load-categories": "Kategorien konnten nicht geladen werden", - "category-is-empty": "Kategorie ist leer", - "could-not-load-manga": "Manga konnte nicht geladen werden", - "your-library-is-empty": "Deine Bibliothek ist leer" - }, - "Browse": { - "sources": "Quellen", - "extensions": "Erweiterungen" - }, - "DownloadQueue": { - "no-downloads": "Keine Downloads", - "download-queue": "Herunterladen-Warteschlange" + "category": { + "error": { + "label": { + "empty": "Kategorie ist leer", + "request_failure": "Kategorien konnten nicht geladen werden" + } } + }, + "download": { + "queue": { + "label": { + "no_downloads": "Keine Downloads" + }, + "title": "Herunterladen-Warteschlange" + } + }, + "extension": { + "title": "Erweiterungen" + }, + "library": { + "error": { + "label": { + "empty": "Deine Bibliothek ist leer" + } + }, + "title": "Bibliotek" + }, + "manga": { + "error": { + "label": { + "request_failure": "Manga konnte nicht geladen werden" + } + } + }, + "source": { + "title": "Quellen" } } diff --git a/src/i18n/resources/es.json b/src/i18n/resources/es.json index 8fa868d0..be5e0e64 100644 --- a/src/i18n/resources/es.json +++ b/src/i18n/resources/es.json @@ -1,19 +1,39 @@ { - "screens": { - "Library": { - "Library": "Biblioteca", - "could-not-load-categories": "No se pudieron cargar las categorías", - "your-library-is-empty": "Tu biblioteca esta vacia", - "category-is-empty": "La categoría está vacía", - "could-not-load-manga": "No se pudo cargar el manga" - }, - "DownloadQueue": { - "no-downloads": "No hay descargas", - "download-queue": "Descarga en la cola" - }, - "Browse": { - "extensions": "Extensiones", - "sources": "Fuentes" + "category": { + "error": { + "label": { + "empty": "La categoría está vacía", + "request_failure": "No se pudieron cargar las categorías" + } } + }, + "download": { + "queue": { + "label": { + "no_downloads": "No hay descargas" + }, + "title": "Descarga en la cola" + } + }, + "extension": { + "title": "Extensiones" + }, + "library": { + "error": { + "label": { + "empty": "Tu biblioteca esta vacia" + } + }, + "title": "Biblioteca" + }, + "manga": { + "error": { + "label": { + "request_failure": "No se pudo cargar el manga" + } + } + }, + "source": { + "title": "Fuentes" } } diff --git a/src/i18n/resources/fr.json b/src/i18n/resources/fr.json index 5e01d6c7..7023e31d 100644 --- a/src/i18n/resources/fr.json +++ b/src/i18n/resources/fr.json @@ -1,19 +1,39 @@ { - "screens": { - "Library": { - "Library": "Bibliothèque", - "could-not-load-categories": "Impossible de charger les catégories", - "your-library-is-empty": "Votre bibliothèque est vide", - "category-is-empty": "La catégorie est vide", - "could-not-load-manga": "Impossible de charger le manga" - }, - "Browse": { - "sources": "Sources", - "extensions": "Extensions" - }, - "DownloadQueue": { - "no-downloads": "Aucun téléchargement", - "download-queue": "File d’attente des téléchargements" + "category": { + "error": { + "label": { + "empty": "La catégorie est vide", + "request_failure": "Impossible de charger les catégories" + } } + }, + "download": { + "queue": { + "label": { + "no_downloads": "Aucun téléchargement" + }, + "title": "File d’attente des téléchargements" + } + }, + "extension": { + "title": "Extensions" + }, + "library": { + "error": { + "label": { + "empty": "Votre bibliothèque est vide" + } + }, + "title": "Bibliothèque" + }, + "manga": { + "error": { + "label": { + "request_failure": "Impossible de charger le manga" + } + } + }, + "source": { + "title": "Sources" } } diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 4ee82e93..b4192d3a 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -7,12 +7,12 @@ import en from 'i18n/locale/en.json'; -const translationHelper = (lng: any) => ({ +const translationHelper = (lng: T) => ({ translation: lng, }); const resources = { en: translationHelper(en), -}; +} as const; export default resources; diff --git a/src/screens/Browse.tsx b/src/screens/Browse.tsx index bc5b477e..43c460d0 100644 --- a/src/screens/Browse.tsx +++ b/src/screens/Browse.tsx @@ -30,8 +30,8 @@ export default function Browse() { scrollButtons allowScrollButtonsMobile > - - + + diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index 762d5de5..3fe31bf8 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -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 ; + return ; } const handleDelete = (chapter: IChapter) => { diff --git a/src/screens/Extensions.tsx b/src/screens/Extensions.tsx index 9dc56683..7a1b6579 100644 --- a/src/screens/Extensions.tsx +++ b/src/screens/Extensions.tsx @@ -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(null); const { setTitle, setAction } = useContext(NavbarContext); const [shownLangs, setShownLangs] = useLocalStorage('shownExtensionLangs', extensionDefaultLangs()); @@ -75,7 +78,7 @@ export default function MangaExtensions() { const [query] = useQueryParam('query', StringParam); useEffect(() => { - setTitle('Extensions'); + setTitle(t('extension.title')); setAction( <> @@ -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'); } }; diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index dc27be57..251f17fa 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -41,7 +41,7 @@ export default function Library() { const { setTitle, setAction } = useContext(NavbarContext); useEffect(() => { - setTitle(t('screens.Library.Library')); + setTitle(t('library.title')); setAction( <> @@ -62,7 +62,7 @@ export default function Library() { if (tabsError != null) { return ( ); @@ -73,7 +73,7 @@ export default function Library() { } if (tabs.length === 0) { - return ; + return ; } if (tabs.length === 1) { @@ -81,7 +81,7 @@ export default function Library() { ); @@ -112,14 +112,14 @@ export default function Library() { {tab === activeTab && (mangaError ? ( ) : ( ))} diff --git a/src/screens/Manga.tsx b/src/screens/Manga.tsx index d6e298e9..fa191ca0 100644 --- a/src/screens/Manga.tsx +++ b/src/screens/Manga.tsx @@ -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 ; + return ; } return ( @@ -72,7 +75,7 @@ const Manga: React.FC = () => { - Could not fetch manga data + {t('manga.error.label.request_failure')}
{error.message ?? error} diff --git a/src/screens/Reader.tsx b/src/screens/Reader.tsx index 708f48bb..3c31706f 100644 --- a/src/screens/Reader.tsx +++ b/src/screens/Reader.tsx @@ -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('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}`); } diff --git a/src/screens/SearchAll.tsx b/src/screens/SearchAll.tsx index 8730ace4..60be2268 100644 --- a/src/screens/SearchAll.tsx +++ b/src/screens/SearchAll.tsx @@ -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(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(); }, []); @@ -146,7 +149,7 @@ const SearchAll: React.FC = () => { }, []); useEffect(() => { - setTitle('Sources'); + setTitle(t('source.title')); setAction( <> @@ -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 /> diff --git a/src/screens/Settings.tsx b/src/screens/Settings.tsx index 42da79f3..5bbfe409 100644 --- a/src/screens/Settings.tsx +++ b/src/screens/Settings.tsx @@ -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() { - + - + - + - + setDarkTheme(!darkTheme)} /> @@ -128,7 +131,7 @@ export default function Settings() { { handleDialogOpenItemWidth(); @@ -139,7 +142,10 @@ export default function Settings() { - + setShowNsfw(!showNsfw)} /> @@ -149,9 +155,8 @@ export default function Settings() { setUseCache(!useCache)} /> @@ -161,7 +166,7 @@ export default function Settings() { - + { @@ -177,18 +182,18 @@ export default function Settings() { - + - Enter Server Address + {t('settings.server_address.dialog.label.enter_address')} - Manga Item width + {t('settings.label.manga_item_width')} diff --git a/src/screens/SourceConfigure.tsx b/src/screens/SourceConfigure.tsx index 95bd24fa..ea4d40ab 100644 --- a/src/screens/SourceConfigure.tsx +++ b/src/screens/SourceConfigure.tsx @@ -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); }, []); diff --git a/src/screens/SourceMangas.tsx b/src/screens/SourceMangas.tsx index 46904633..b5916bb1 100644 --- a/src/screens/SourceMangas.tsx +++ b/src/screens/SourceMangas.tsx @@ -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 = ( <> - Check out - Local source guide + {t('source.local_source.label.checkout')} + + {t('source.local_source.label.guide')} + ); } diff --git a/src/screens/Sources.tsx b/src/screens/Sources.tsx index d84c7535..5bf91f5e 100644 --- a/src/screens/Sources.tsx +++ b/src/screens/Sources.tsx @@ -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('shownSourceLangs', sourceDefualtLangs()); @@ -70,7 +72,7 @@ export default function Sources() { }, []); useEffect(() => { - setTitle('Sources'); + setTitle(t('source.title')); setAction( <> history.push('/sources/all/search/')} size="large"> @@ -89,7 +91,7 @@ export default function Sources() { if (loading) return ; if (sources?.length === 0) { - return

No sources found. Install Some Extensions first.

; + return

{t('source.error.label.no_sources_found')}

; } return ( diff --git a/src/screens/Updates.tsx b/src/screens/Updates.tsx index ae34de12..97825935 100644 --- a/src/screens/Updates.tsx +++ b/src/screens/Updates.tsx @@ -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 ; } if (fetched && updateEntries.length === 0) { - return ; + return ; } const downloadForChapter = (chapter: IChapter) => { diff --git a/src/screens/settings/About.tsx b/src/screens/settings/About.tsx index c107a695..9bfab533 100644 --- a/src/screens/settings/About.tsx +++ b/src/screens/settings/About.tsx @@ -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 ( - + - + - + - + - + ); diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index 0053db98..cd560a5f 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -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() { <> - + document.getElementById('backup-file')?.click()}> diff --git a/src/screens/settings/Categories.tsx b/src/screens/settings/Categories.tsx index d2bb7813..535a3b24 100644 --- a/src/screens/settings/Categories.tsx +++ b/src/screens/settings/Categories.tsx @@ -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() { - {categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'} + {categoryToEdit === -1 + ? t('category.dialog.title.new_category') + : t('category.dialog.title.edit_category')} } - label="Default category when adding new manga to library" + label={t('category.label.use_as_default_category')} /> diff --git a/src/screens/settings/DefaultReaderSettings.tsx b/src/screens/settings/DefaultReaderSettings.tsx index bb0cc74c..d1320e70 100644 --- a/src/screens/settings/DefaultReaderSettings.tsx +++ b/src/screens/settings/DefaultReaderSettings.tsx @@ -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'), ); }; diff --git a/src/screens/settings/SearchSettings.tsx b/src/screens/settings/SearchSettings.tsx index 3d9340dd..6858e9a9 100644 --- a/src/screens/settings/SearchSettings.tsx +++ b/src/screens/settings/SearchSettings.tsx @@ -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() { - + >; IconComponent: OverridableComponent>; show: 'mobile' | 'desktop' | 'both'; diff --git a/src/util/date.ts b/src/util/date.ts index d19495df..1daac10d 100644 --- a/src/util/date.ts +++ b/src/util/date.ts @@ -5,6 +5,7 @@ * 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'; export const isWithinLastXMillis = (date: Date, timeMS: number) => { const timeDifference = Date.now() - date.getTime(); @@ -54,11 +55,11 @@ export const getUploadDateString = (date: Date | number) => { : ''; if (wasUploadedToday) { - return `Today at ${timeString}`; + return t('global.date.label.today_at', { timeString }); } if (wasUploadedYesterday) { - return `Yesterday at ${timeString}`; + return t('global.date.label.yesterday_at', { timeString }); } return uploadDate.toLocaleDateString(undefined, { diff --git a/src/util/language.tsx b/src/util/language.tsx index a0120824..075c81ee 100644 --- a/src/util/language.tsx +++ b/src/util/language.tsx @@ -4,6 +4,7 @@ * 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/. */ +import { t } from 'i18next'; export const ISOLanguages = [ { code: 'all', name: 'All', nativeName: 'All' }, @@ -80,7 +81,7 @@ export function langCodeToName(code: string): string { const whereToCut = code.indexOf('-') !== -1 ? code.indexOf('-') : code.length; const proccessedCode = code.toLocaleLowerCase().substring(0, whereToCut); - let result = `language with code: ${code}`; + let result = t('global.language.label.language_with_code', { code }); for (let i = 0; i < ISOLanguages.length; i++) { if (ISOLanguages[i].code === proccessedCode || ISOLanguages[i].code === code.toLocaleLowerCase()) {