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

@@ -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} />