Migrate to lingui

Switch to "lingui" for better DX.
Tried to persist existing languages as much as possible.

Removed "vite-plugin-node-polyfills" because it's incompatible with "lingui"
This commit is contained in:
schroda
2026-01-10 19:45:02 +01:00
parent c1ac58f4d6
commit 65a9a905be
287 changed files with 120766 additions and 37748 deletions

View File

@@ -6,6 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { MangaStatus } from '@/lib/graphql/generated/graphql.ts';
import { MangaAction, MangaIdInfo, MangaType } from '@/features/manga/Manga.types.ts';
import {
@@ -13,7 +15,6 @@ import {
CHAPTER_ACTION_TO_TRANSLATION,
} from '@/features/chapter/Chapter.constants.ts';
import { GqlMetaHolder } from '@/features/metadata/Metadata.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const FALLBACK_MANGA: MangaIdInfo & GqlMetaHolder = { id: -1 };
@@ -21,14 +22,14 @@ export const GLOBAL_READER_SETTINGS_MANGA: MangaIdInfo = { id: -2 };
export const MANGA_COVER_ASPECT_RATIO = '1 / 1.5';
export const MANGA_STATUS_TO_TRANSLATION: Record<MangaStatus, TranslationKey> = {
[MangaStatus.Cancelled]: 'manga.status.cancelled',
[MangaStatus.Completed]: 'manga.status.completed',
[MangaStatus.Licensed]: 'manga.status.licensed',
[MangaStatus.Ongoing]: 'manga.status.ongoing',
[MangaStatus.OnHiatus]: 'manga.status.hiatus',
[MangaStatus.PublishingFinished]: 'manga.status.publishing_finished',
[MangaStatus.Unknown]: 'manga.status.unknown',
export const MANGA_STATUS_TO_TRANSLATION: Record<MangaStatus, MessageDescriptor> = {
[MangaStatus.Cancelled]: msg`Cancelled`,
[MangaStatus.Completed]: msg`Completed`,
[MangaStatus.Licensed]: msg`Licensed`,
[MangaStatus.Ongoing]: msg`Ongoing`,
[MangaStatus.OnHiatus]: msg`Hiatus`,
[MangaStatus.PublishingFinished]: msg`Publishing finished`,
[MangaStatus.Unknown]: msg`Unknown`,
};
export const MANGA_ACTION_TO_CONFIRMATION_REQUIRED: Record<
@@ -45,48 +46,48 @@ export const MANGA_ACTION_TO_CONFIRMATION_REQUIRED: Record<
export const MANGA_ACTION_TO_TRANSLATION: {
[key in MangaAction]: {
action: {
single: TranslationKey;
selected: TranslationKey;
single: MessageDescriptor;
selected: MessageDescriptor;
};
confirmation?: TranslationKey;
success: TranslationKey;
error: TranslationKey;
confirmation?: MessageDescriptor;
success: MessageDescriptor;
error: MessageDescriptor;
};
} = {
...CHAPTER_ACTION_TO_TRANSLATION,
remove_from_library: {
action: {
single: 'manga.action.library.remove.label.action',
selected: 'manga.action.library.remove.button.selected',
single: msg`Remove from the library`,
selected: msg`Remove selected from the library`,
},
confirmation: 'manga.action.library.remove.label.confirmation',
success: 'manga.action.library.remove.label.success',
error: 'manga.action.library.remove.label.error',
confirmation: msg`{count, plural, one {You are about to remove one entry from your library} other {You are about to remove # entries from your library}}`,
success: msg`{count, plural, one {Removed manga from the library} other {Removed # manga from the library}}`,
error: msg`{count, plural, one {Could not remove manga from the library} other {Could not remove manga from the library}}`,
},
change_categories: {
action: {
single: 'manga.action.category.label.action',
selected: 'manga.action.category.button.selected',
single: msg`Change categories`,
selected: msg`Change categories of selected`,
},
confirmation: 'manga.action.category.label.confirmation',
success: 'manga.action.category.label.success',
error: 'manga.action.category.label.error',
confirmation: msg`{count, plural, one {You are about to change the category of one entry} other {You are about to change the category of # entries}}`,
success: msg`{count, plural, one {Changed categories of manga} other {Changed categories of # manga}}`,
error: msg`{count, plural, one {Could not change the categories of the manga} other {Could not change the categories of the manga}}`,
},
migrate: {
action: {
single: 'global.button.migrate',
selected: 'global.button.migrate', // not supported
single: msg`Migrate`,
selected: msg`Migrate`, // not supported
},
success: 'manga.action.migrate.label.success',
error: 'manga.action.migrate.label.error',
success: msg`Successfully migrated manga`,
error: msg`Could not migrate manga`,
},
track: {
action: {
single: 'manga.action.track.add.label.action',
selected: 'manga.action.track.add.label.action', // not supported
single: msg`Track`,
selected: msg`Track`, // not supported
},
success: 'manga.action.track.add.label.success',
error: 'manga.action.track.add.label.error',
success: msg`Tracked manga`,
error: msg`Could not track manga`,
},
};
@@ -151,10 +152,10 @@ export const SOURCES_BY_MANGA_TYPE: Record<MangaType, string[]> = {
],
};
export const MANGA_TAGS_BY_MANGA_TYPE: Record<MangaType, TranslationKey[]> = {
[MangaType.MANGA]: ['manga.type.manga'],
[MangaType.COMIC]: ['manga.type.comic'],
[MangaType.WEBTOON]: ['manga.type.webtoon', 'manga.type.long_strip'],
[MangaType.MANHWA]: ['manga.type.manhwa', 'manga.type.long_strip'],
[MangaType.MANHUA]: ['manga.type.manhua', 'manga.type.long_strip'],
export const MANGA_TAGS_BY_MANGA_TYPE: Record<MangaType, MessageDescriptor[]> = {
[MangaType.MANGA]: [msg`Manga`],
[MangaType.COMIC]: [msg`Comic`],
[MangaType.WEBTOON]: [msg`Webtoon`, msg`Long strip`],
[MangaType.MANHWA]: [msg`Manhwa`, msg`Long strip`],
[MangaType.MANHUA]: [msg`Manhua`, msg`Long strip`],
};

View File

@@ -7,7 +7,7 @@
*/
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip';
import {
ChapterNameInfo,
@@ -28,7 +28,7 @@ export const ContinueReadingTooltip = ({
ChapterScanlatorInfo & {
children: ReactElement;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const isFirstChapter = sourceOrder === 1;
@@ -41,11 +41,11 @@ export const ContinueReadingTooltip = ({
},
},
}}
title={t(isFirstChapter ? 'chapter.action.read.start' : 'chapter.action.read.resume', {
chapterNumber,
title: name,
scanlator,
})}
title={
isFirstChapter
? t`Start reading\n#${chapterNumber}${name}\n${scanlator}`
: t`Continue reading\n#${chapterNumber}${name}\n${scanlator}`
}
>
{children}
</CustomTooltip>

View File

@@ -11,7 +11,6 @@ import Delete from '@mui/icons-material/Delete';
import Download from '@mui/icons-material/Download';
import RemoveDone from '@mui/icons-material/RemoveDone';
import Done from '@mui/icons-material/Done';
import { useTranslation } from 'react-i18next';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import Label from '@mui/icons-material/Label';
import { useMemo, useState } from 'react';
@@ -20,6 +19,7 @@ import { Link } from 'react-router-dom';
import SyncIcon from '@mui/icons-material/Sync';
import Dialog from '@mui/material/Dialog';
import { AwaitableComponent } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
import { MenuItem } from '@/base/components/menu/MenuItem.tsx';
@@ -60,7 +60,7 @@ export const MangaActionMenuItems = ({
onClose,
setHideMenu,
}: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isTrackDialogOpen, setIsTrackDialogOpen] = useState(false);
@@ -108,7 +108,7 @@ export const MangaActionMenuItems = ({
return (
<>
{!!handleSelection && isSingleMode && (
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t`Select`} />
)}
{shouldShowMenuItem(!isFullyDownloaded) && (
<NestedMenuItem

View File

@@ -7,9 +7,9 @@
*/
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { MangaCardMode } from '@/features/manga/Manga.types.ts';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
@@ -42,7 +42,7 @@ export const MangaBadges = ({
downloadCount?: number;
mode: MangaCardMode;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const isTouchDevice = MediaQuery.useIsTouchDevice();
@@ -69,7 +69,7 @@ export const MangaBadges = ({
}}
color={isInLibrary ? 'error' : 'primary'}
>
{t(isInLibrary ? 'manga.action.library.remove.label.action' : 'manga.button.add_to_library')}
{isInLibrary ? t`Remove from the library` : t`Add To Library`}
</Button>
)}
{inLibraryIndicator && isInLibrary && (
@@ -77,7 +77,7 @@ export const MangaBadges = ({
className="source-manga-library-state-indicator"
sx={{ backgroundColor: 'primary.dark', color: 'primary.contrastText', p: 0.3 }}
>
{t('manga.button.in_library')}
{t`In Library`}
</Typography>
)}
{((showUnreadBadge && mode === 'default') || mode === 'duplicate') && (unread ?? 0) > 0 && (

View File

@@ -10,7 +10,7 @@ import React, { ForwardedRef, Ref, useCallback, useLayoutEffect, useMemo, useRef
import Grid, { GridTypeMap } from '@mui/material/Grid';
import Box, { BoxProps } from '@mui/material/Box';
import { GridItemProps } from 'react-virtuoso';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { MangaCard } from '@/features/manga/components/cards/MangaCard.tsx';
@@ -209,7 +209,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
retry,
gridWrapperProps,
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const { navBarWidth } = useNavBarContext();
const {
@@ -313,7 +313,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
return (
<EmptyViewAbsoluteCentered
noFaces={noFaces}
message={message ?? t('manga.error.label.no_mangas_found')}
message={message ?? t`No manga found`}
messageExtra={messageExtra}
retry={retry}
/>

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { BaseSyntheticEvent, ChangeEvent, useMemo, ForwardedRef } from 'react';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
@@ -14,6 +13,7 @@ import IconButton from '@mui/material/IconButton';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { PopupState } from 'material-ui-popup-state/hooks';
import { bindTrigger } from 'material-ui-popup-state';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
@@ -34,7 +34,7 @@ export const MangaOptionButton = ({
popupState: PopupState;
ref?: ForwardedRef<HTMLButtonElement | null>;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]);
@@ -59,7 +59,7 @@ export const MangaOptionButton = ({
}
return (
<CustomTooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
<CustomTooltip title={selected ? t`Deselect` : t`Select`}>
<Checkbox {...MUIUtil.preventRippleProp()} checked={selected} onChange={handleSelectionChange} />
</CustomTooltip>
);
@@ -67,7 +67,7 @@ export const MangaOptionButton = ({
if (asCheckbox) {
return (
<CustomTooltip title={t('global.button.options')}>
<CustomTooltip title={t`Options`}>
<IconButton
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
@@ -80,7 +80,7 @@ export const MangaOptionButton = ({
}
return (
<CustomTooltip title={t('global.button.options')}>
<CustomTooltip title={t`Options`}>
<Button
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}

View File

@@ -15,13 +15,13 @@ import ListItemText from '@mui/material/ListItemText';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { AwaitableComponent } from 'awaitable-component';
import ColorLensIcon from '@mui/icons-material/ColorLens';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
@@ -40,7 +40,7 @@ interface IProps {
}
export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
@@ -78,7 +78,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<>
{isLargeScreen && (
<>
<CustomTooltip title={t('manga.label.reload_from_source')} disabled={refreshing}>
<CustomTooltip title={t`Reload data from source`} disabled={refreshing}>
<IconButton
onClick={() => {
onRefresh();
@@ -90,7 +90,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
</IconButton>
</CustomTooltip>
{settings.mangaDynamicColorSchemes && (
<CustomTooltip title={t('settings.appearance.manga_dynamic_color_schemes.save')}>
<CustomTooltip title={t`Save dynamic color theme`}>
<IconButton onClick={saveDynamicColorTheme} color="inherit">
<ColorLensIcon />
</IconButton>
@@ -98,7 +98,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
)}
{manga.inLibrary && (
<>
<CustomTooltip title={t('global.button.migrate')}>
<CustomTooltip title={t`Migrate`}>
<Link
to={AppRoutes.migrate.childRoutes.search.path(
manga.sourceId,
@@ -113,7 +113,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
</IconButton>
</Link>
</CustomTooltip>
<CustomTooltip title={t('manga.label.edit_categories')}>
<CustomTooltip title={t`Edit manga categories`}>
<IconButton
onClick={() => {
openCategorySelection();
@@ -158,14 +158,14 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<ListItemIcon>
<Refresh fontSize="small" />
</ListItemIcon>
<ListItemText>{t('manga.label.reload_from_source')}</ListItemText>
<ListItemText>{t`Reload data from source`}</ListItemText>
</MenuItem>
{settings.mangaDynamicColorSchemes && (
<MenuItem onClick={saveDynamicColorTheme}>
<ListItemIcon>
<ColorLensIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{t('settings.appearance.manga_dynamic_color_schemes.save')}</ListItemText>
<ListItemText>{t`Save dynamic color theme`}</ListItemText>
</MenuItem>
)}
{manga.inLibrary && [
@@ -179,7 +179,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<ListItemIcon>
<SyncAltIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{t('migrate.title')}</ListItemText>
<ListItemText>{t`Migrate`}</ListItemText>
</MenuItem>,
<MenuItem
key="categories"
@@ -191,7 +191,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<ListItemIcon>
<Label fontSize="small" />
</ListItemIcon>
<ListItemText>{t('manga.label.edit_categories')}</ListItemText>
<ListItemText>{t`Edit manga categories`}</ListItemText>
</MenuItem>,
]}
</Menu>

View File

@@ -8,7 +8,7 @@
import { Link } from 'react-router-dom';
import PlayArrow from '@mui/icons-material/PlayArrow';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { StyledFab } from '@/base/components/buttons/StyledFab.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import {
@@ -31,7 +31,7 @@ export function ResumeFab({
ChapterNameInfo &
ChapterScanlatorInfo;
}) {
const { t } = useTranslation();
const { t } = useLingui();
const { sourceOrder, name, chapterNumber, scanlator } = chapter;
const isFirstChapter = sourceOrder === 1;
@@ -51,7 +51,7 @@ export function ResumeFab({
state={Chapters.getReaderOpenChapterLocationState(chapter)}
>
<PlayArrow />
{isFirstChapter ? t('global.button.start') : t('global.button.resume')}
{isFirstChapter ? t`Start` : t`Resume`}
</StyledFab>
</ContinueReadingTooltip>
);

View File

@@ -8,10 +8,11 @@
import { useNavigate } from 'react-router-dom';
import SyncIcon from '@mui/icons-material/Sync';
import { useTranslation } from 'react-i18next';
import PopupState, { bindDialog, bindTrigger } from 'material-ui-popup-state';
import Dialog from '@mui/material/Dialog';
import CheckIcon from '@mui/icons-material/Check';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { TrackManga } from '@/features/tracker/components/TrackManga.tsx';
@@ -23,7 +24,7 @@ import { MangaTrackRecordInfo } from '@/features/manga/Manga.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick<MangaType, 'title'> }) => {
const { t } = useTranslation();
const { t } = useLingui();
const navigate = useNavigate();
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
@@ -35,7 +36,7 @@ export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick
const handleClick = (openPopup: () => void) => {
if (trackerList.error) {
makeToast(t('tracking.error.label.could_not_load_track_info'), 'error', trackerList.error?.toString());
makeToast(t`Could not load track info`, 'error', trackerList.error?.toString());
return;
}
@@ -60,8 +61,11 @@ export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick
>
{trackersInUse.length ? <CheckIcon /> : <SyncIcon />}
{trackersInUse.length
? t('manga.button.track.active', { count: trackersInUse.length })
: t('manga.button.track.start')}
? plural(trackersInUse.length, {
one: '# Tracker',
other: '# Tracker',
})
: t`Tracking`}
</CustomButton>
{popupState.isOpen && (
<Dialog {...bindDialog(popupState)} maxWidth="md" fullWidth scroll="paper">

View File

@@ -10,14 +10,14 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import { styled } from '@mui/material/styles';
import { ComponentProps, ReactNode, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ButtonGroup from '@mui/material/ButtonGroup';
import { useLingui } from '@lingui/react/macro';
import { t as translate } from '@lingui/core/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { Mangas } from '@/features/manga/services/Mangas.ts';
@@ -136,11 +136,11 @@ const MangaButtonsContainer = styled('div')(({ theme }) => ({
}));
const OpenSourceButton = ({ url }: { url?: string | null }) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<ButtonGroup>
<CustomTooltip title={t('global.button.open_browser')} disabled={!url}>
<CustomTooltip title={t`Open in browser`} disabled={!url}>
<CustomButtonIcon
size="medium"
disabled={!url}
@@ -153,7 +153,7 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
<IconBrowser />
</CustomButtonIcon>
</CustomTooltip>
<CustomTooltip title={t('global.button.open_webview')} disabled={!url}>
<CustomTooltip title={t`Open in WebView`} disabled={!url}>
<CustomButtonIcon
size="medium"
disabled={!url}
@@ -172,11 +172,11 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null): string {
if (!source) {
return translate('global.label.unknown');
return translate`Unknown`;
}
if (Sources.isLocalSource(source)) {
return translate('source.local_source.title');
return translate`Local source`;
}
return source.displayName ?? source.id;
@@ -215,7 +215,7 @@ export const MangaDetails = ({
};
mode: MangaLocationState['mode'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const {
settings: { mangaThumbnailBackdrop, mangaDynamicColorSchemes },
@@ -223,16 +223,16 @@ export const MangaDetails = ({
useEffect(() => {
if (!manga.source) {
makeToast(translate('source.error.label.source_not_found'), 'error');
makeToast(t`Could not find source. Check your installed extensions.`, 'error');
}
}, [manga.source]);
}, [manga.source, t]);
const { updateLibraryState } = useManageMangaLibraryState(manga);
const copyTitle = async () => {
try {
await navigator.clipboard.writeText(manga.title);
makeToast(t('global.label.copied_clipboard'), 'info');
makeToast(t`Copied to clipboard`, 'info');
} catch (e) {
defaultPromiseErrorHandler('MangaDetails::copyTitleLongPress')(e);
}
@@ -250,7 +250,7 @@ export const MangaDetails = ({
{manga.title}
</Typography>
</SearchLink>
<CustomTooltip title={t('global.button.copy')}>
<CustomTooltip title={t`Copy`}>
<IconButton onClick={copyTitle} color="inherit">
<ContentCopyIcon fontSize="small" />
</IconButton>
@@ -258,21 +258,18 @@ export const MangaDetails = ({
</Stack>
{manga.author && (
<Metadata
title={t('manga.label.author')}
title={t`Author`}
value={valuesToJoinedSearchLinks(Mangas.getAuthors(manga), manga.source?.id, mode)}
/>
)}
{manga.artist && (
<Metadata
title={t('manga.label.artist')}
title={t`Artist`}
value={valuesToJoinedSearchLinks(Mangas.getArtists(manga), manga.source?.id, mode)}
/>
)}
<Metadata
title={t('manga.label.status')}
value={t(MANGA_STATUS_TO_TRANSLATION[manga.status])}
/>
<Metadata title={t('source.title_one')} value={getSourceName(manga.source)} />
<Metadata title={t`Status`} value={t(MANGA_STATUS_TO_TRANSLATION[manga.status])} />
<Metadata title={t`Source`} value={getSourceName(manga.source)} />
</MetadataContainer>
</ThumbnailMetadataWrapper>
<MangaButtonsContainer>
@@ -282,7 +279,7 @@ export const MangaDetails = ({
variant={manga.inLibrary ? 'contained' : 'outlined'}
>
{manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
{manga.inLibrary ? t`In Library` : t`Add To Library`}
</CustomButton>
<TrackMangaButton manga={manga} />
<OpenSourceButton url={manga.realUrl} />

View File

@@ -7,9 +7,9 @@
*/
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import gql from 'graphql-tag';
import { AwaitableComponent } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
@@ -27,7 +27,7 @@ export const useManageMangaLibraryState = (
manga: Pick<MangaType, 'id' | 'title'> & Partial<Pick<MangaType, 'inLibrary'>>,
confirmRemoval: boolean = false,
) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary);
@@ -42,10 +42,10 @@ export const useManageMangaLibraryState = (
updateManga: { inLibrary: true },
updateMangaCategories: { addToCategories, removeFromCategories },
})
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
.response.then(() => makeToast(t`Added manga to library!`, 'success'))
.then(() => setIsInLibrary(true))
.catch((e) => {
makeToast(t('library.error.label.add_to_library'), 'error', getErrorMessage(e));
makeToast(t`Could not add manga to library!`, 'error', getErrorMessage(e));
});
},
[manga.id],
@@ -55,10 +55,12 @@ export const useManageMangaLibraryState = (
if (confirmRemoval) {
await Confirmation.show(
{
title: t('global.label.are_you_sure'),
message: t('manga.action.library.remove.dialog.label.message', { title: manga.title }),
title: t`Are you sure?`,
message: t`You are about to remove "${manga.title}" from your library`,
actions: {
confirm: { title: t('global.button.remove') },
confirm: {
title: t`Remove`,
},
},
},
{ id: `manga-library-state-remove-${manga.id}` },
@@ -83,7 +85,7 @@ export const useManageMangaLibraryState = (
showAddToLibraryCategorySelectDialog = (await getMetadataServerSettings())
.showAddToLibraryCategorySelectDialog;
} catch (e) {
makeToast(t('global.error.label.failed_to_load_data'), 'error', getErrorMessage(e));
makeToast(t`Unable to load data`, 'error', getErrorMessage(e));
return;
}
@@ -98,7 +100,7 @@ export const useManageMangaLibraryState = (
GetCategoriesBaseQueryVariables
>(GET_CATEGORIES_BASE).response;
} catch (e) {
makeToast(t('category.error.label.request_failure'), 'error', getErrorMessage(e));
makeToast(t`Could not load categories`, 'error', getErrorMessage(e));
return;
}
const userCreatedCategories = Categories.getUserCreated(categories.data.categories.nodes);
@@ -109,15 +111,20 @@ export const useManageMangaLibraryState = (
try {
duplicatedLibraryMangas = await Mangas.getDuplicateLibraryMangas(manga.title).response;
} catch (e) {
const errorMessage = getErrorMessage(e);
await Confirmation.show(
{
title: t('global.error.label.failed_to_load_data'),
message: t('manga.action.library.add.dialog.duplicate.label.failure', {
error: getErrorMessage(e),
}),
title: t`Unable to load data`,
message: t`Could not check for duplicated manga in your library.\n\nError: ${errorMessage}`,
actions: {
extra: { show: true, title: t('global.button.retry'), contain: true },
confirm: { title: t('global.button.add') },
extra: {
show: true,
title: t`Retry`,
contain: true,
},
confirm: {
title: t`Add`,
},
},
onExtra: () =>
update().catch(
@@ -132,16 +139,18 @@ export const useManageMangaLibraryState = (
if (doDuplicatesExist) {
await Confirmation.show(
{
title: t('global.label.are_you_sure'),
message: t('manga.action.library.add.dialog.duplicate.label.info'),
title: t`Are you sure?`,
message: t`You have an entry in your library with the same name.`,
actions: {
extra: {
show: true,
title: t('migrate.dialog.action.button.show_entry'),
title: t`Show entry`,
contain: true,
link: AppRoutes.manga.path(duplicatedLibraryMangas!.data.mangas.nodes[0].id),
},
confirm: { title: t('global.button.add') },
confirm: {
title: t`Add`,
},
},
onExtra: () => {},
},

View File

@@ -12,9 +12,9 @@ import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ChapterList } from '@/features/chapter/components/ChapterList.tsx';
@@ -30,7 +30,7 @@ import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitl
import { MangaLocationState } from '@/features/manga/Manga.types.ts';
export const Manga: React.FC = () => {
const { t } = useTranslation();
const { t } = useLingui();
const { id } = useParams<{ id: string }>();
const { mode } = useLocation<MangaLocationState>().state ?? {};
@@ -61,7 +61,7 @@ export const Manga: React.FC = () => {
}, [manga]);
useAppTitleAndAction(
manga?.title ?? t('manga.title_one'),
manga?.title ?? t`Manga`,
<Stack
direction="row"
sx={{
@@ -72,7 +72,7 @@ export const Manga: React.FC = () => {
<CustomTooltip
title={
<>
{t('manga.error.label.request_failure')}
{t`Could not load manga`}
<br />
{getErrorMessage(error)}
</>
@@ -94,12 +94,7 @@ export const Manga: React.FC = () => {
);
if (error && !manga) {
return (
<EmptyViewAbsoluteCentered
message={t('manga.error.label.request_failure')}
messageExtra={getErrorMessage(error)}
/>
);
return <EmptyViewAbsoluteCentered message={t`Could not load manga`} messageExtra={getErrorMessage(error)} />;
}
return (
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>

View File

@@ -6,8 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import i18next, { t as translate } from 'i18next';
import { DocumentNode, Unmasked } from '@apollo/client/core';
import { t } from '@lingui/core/macro';
import { i18n } from '@/i18n';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import {
ChapterConditionInput,
@@ -557,11 +558,12 @@ export class Mangas {
try {
await Confirmation.show({
title: translate('global.label.are_you_sure'),
message: translate(confirmationMessage, { count: itemCount }),
title: t`Are you sure?`,
/* lingui-extract-ignore */
message: i18n.t({ ...confirmationMessage, values: { count: itemCount } }),
actions: {
confirm: {
title: translate('global.button.ok'),
title: t`Ok`,
},
},
});
@@ -571,10 +573,15 @@ export class Mangas {
}
await fnToExecute();
makeToast(translate(MANGA_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success');
makeToast(
/* lingui-extract-ignore */
i18n.t({ ...MANGA_ACTION_TO_TRANSLATION[action].success, values: { count: itemCount } }),
'success',
);
} catch (e) {
makeToast(
translate(MANGA_ACTION_TO_TRANSLATION[action].error, { count: itemCount }),
/* lingui-extract-ignore */
i18n.t({ ...MANGA_ACTION_TO_TRANSLATION[action].error, values: { count: itemCount } }),
'error',
getErrorMessage(e),
);
@@ -650,9 +657,14 @@ export class Mangas {
const translateMangaTagsByMangaTypeEntries = Object.entries(MANGA_TAGS_BY_MANGA_TYPE).map(
([mangaType, tags]) => [
mangaType,
['en', i18next.language, manga.source?.lang]
['en', i18n.locale, manga.source?.lang]
.filter((lng) => !!lng)
.flatMap((language) => tags.flatMap((tag) => translate(tag, { lng: language }))),
.flatMap((language) =>
tags.flatMap((tag) =>
/* lingui-extract-ignore */
i18n.t({ ...tag, values: { lng: language } }),
),
),
],
);
const translatedMangaTagsByMangaType = Object.fromEntries(translateMangaTagsByMangaTypeEntries) as Record<