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,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import ListItemButton from '@mui/material/ListItemButton';
import Chip from '@mui/material/Chip';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import ListItemText from '@mui/material/ListItemText';
import { useLingui } from '@lingui/react/macro';
import { makeToast } from '@/base/utils/Toast.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Trackers } from '@/features/tracker/services/Trackers.ts';
@@ -21,7 +21,7 @@ import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
import { CredentialsLogin } from '@/base/components/modals/LoginDialog.tsx';
export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) => {
const { t } = useTranslation();
const { t } = useLingui();
const isOAuthLogin = !tracker.isLoggedIn && !!tracker.authUrl;
@@ -29,7 +29,7 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
try {
await requestManager.logoutFromTracker(tracker.id).response;
} catch (e) {
makeToast(t('tracking.action.logout.label.failure', { name: tracker.name }), 'error', getErrorMessage(e));
makeToast(t`Could not log out from ${tracker.name}`, 'error', getErrorMessage(e));
}
};
@@ -49,7 +49,7 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
try {
await requestManager.loginTrackerCredentials(tracker.id, username, password).response;
} catch (e) {
makeToast(t('tracking.action.login.label.failure', { name: tracker.name }), 'error', getErrorMessage(e));
makeToast(t`Could not log in to ${tracker.name}`, 'error', getErrorMessage(e));
}
};
@@ -68,12 +68,7 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
const controlled = CredentialsLogin.showControlled(
{
title: t(
Trackers.isLoggedIn(tracker)
? 'tracking.settings.dialog.title.log_out'
: 'tracking.settings.dialog.title.log_in',
{ name: tracker.name },
),
title: Trackers.isLoggedIn(tracker) ? t`Log out from ${tracker.name}` : t`Log in to ${tracker.name}`,
isLoading: false,
isLoggedIn: Trackers.isLoggedIn(tracker),
username: initialUsername,
@@ -90,11 +85,7 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
controlled.submit();
} catch (e) {
makeToast(
t('tracking.action.logout.label.failure', { name: tracker.name }),
'error',
getErrorMessage(e),
);
makeToast(t`Could not log out from ${tracker.name}`, 'error', getErrorMessage(e));
}
return;
@@ -105,11 +96,7 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
controlled.submit();
} catch (e) {
makeToast(
t('tracking.action.login.label.failure', { name: tracker.name }),
'error',
getErrorMessage(e),
);
makeToast(t`Could not log in to ${tracker.name}`, 'error', getErrorMessage(e));
const RETRY_KEY = '__retry__';
const retry = await Promise.race([controlled.promise, Promise.resolve(RETRY_KEY)]);
@@ -145,7 +132,7 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
<ListItemText primary={tracker.name} />
{Trackers.isLoggedIn(tracker) && (
<ListItemSecondaryAction>
<Chip label={t('global.label.logged_in')} color="success" />
<Chip label={t`Logged in`} color="success" />
</ListItemSecondaryAction>
)}
</ListItemButton>

View File

@@ -8,7 +8,6 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
@@ -28,6 +27,7 @@ import PopupState, { bindDialog, bindMenu, bindTrigger } from 'material-ui-popup
import { useMemo, useState } from 'react';
import Badge from '@mui/material/Badge';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Trackers } from '@/features/tracker/services/Trackers.ts';
@@ -63,7 +63,7 @@ const TrackerActiveRemoveBind = ({
onClick: () => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [removeRemoteTracking, setRemoveRemoteTracking] = useState(false);
@@ -71,8 +71,8 @@ const TrackerActiveRemoveBind = ({
onClose();
requestManager
.unbindTracker(trackerRecordId, removeRemoteTracking)
.response.then(() => makeToast(t('manga.action.track.remove.label.success'), 'success'))
.catch((e) => makeToast(t('manga.action.track.remove.label.error'), 'error', getErrorMessage(e)));
.response.then(() => makeToast(t`Untracked manga`, 'success'))
.catch((e) => makeToast(t`Could not untrack manga`, 'error', getErrorMessage(e)));
};
return (
@@ -86,7 +86,7 @@ const TrackerActiveRemoveBind = ({
popupState.open();
}}
>
{t('global.button.remove')}
{t`Remove`}
</MenuItem>
<Dialog
{...bindDialog(popupState)}
@@ -95,18 +95,14 @@ const TrackerActiveRemoveBind = ({
popupState.close();
}}
>
<DialogTitle>
{t('manga.action.track.remove.dialog.label.title', { tracker: tracker.name })}
</DialogTitle>
<DialogTitle>{t`Remove ${tracker.name} tracking?`}</DialogTitle>
<DialogContent dividers>
<Typography>{t('manga.action.track.remove.dialog.label.description')}</Typography>
<Typography>{t`This will remove the tracking locally.`}</Typography>
{tracker.supportsTrackDeletion && (
<FormGroup>
<CheckboxInput
disabled={false}
label={t('manga.action.track.remove.dialog.label.delete_remote_track', {
tracker: tracker.name,
})}
label={t`Also remove from ${tracker.name}`}
checked={removeRemoteTracking}
onChange={(_, checked) => setRemoveRemoteTracking(checked)}
/>
@@ -121,7 +117,7 @@ const TrackerActiveRemoveBind = ({
onClose();
}}
>
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button
onClick={() => {
@@ -130,7 +126,7 @@ const TrackerActiveRemoveBind = ({
removeBind();
}}
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>
@@ -151,7 +147,7 @@ const TrackerUpdatePrivateStatus = ({
closeMenu: () => void;
supportsPrivateTracking: TTrackerActive['supportsPrivateTracking'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
if (!supportsPrivateTracking) {
return null;
@@ -162,13 +158,11 @@ const TrackerUpdatePrivateStatus = ({
onClick={() => {
requestManager
.updateTrackerBind(trackRecordId, { private: !isPrivate })
.response.catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
.response.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
closeMenu();
}}
>
{t(isPrivate ? 'tracking.action.button.track_publicly' : 'tracking.action.button.track_privately')}
{isPrivate ? t`Track publicly` : t`Track privately`}
</MenuItem>
);
};
@@ -183,7 +177,7 @@ const TrackerActiveHeader = ({
tracker: TTrackerActive;
openSearch: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack
@@ -218,7 +212,6 @@ const TrackerActiveHeader = ({
/>
</TrackerActiveLink>
</Badge>
<ListItemButton sx={{ flexGrow: 1 }} onClick={openSearch}>
<CustomTooltip title={trackRecord.title}>
<TypographyMaxLines flexGrow={1} lines={1}>
@@ -243,9 +236,7 @@ const TrackerActiveHeader = ({
key={`tracker-active-menu-item-browser-${tracker.id}`}
url={trackRecord.remoteUrl}
>
<MenuItem onClick={() => onClose()}>
{t('global.label.open_in_browser')}
</MenuItem>
<MenuItem onClick={() => onClose()}>{t`Open in browser`}</MenuItem>
</TrackerActiveLink>,
<TrackerActiveRemoveBind
key={`tracker-active-menu-item-remove-${tracker.id}`}
@@ -287,7 +278,7 @@ export const TrackerActiveCard = ({
tracker: TTrackerBind;
onClick: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const isScoreUnset = isUnsetScore(trackRecord.displayScore);
const currentScore = isScoreUnset ? tracker.scores[0] : trackRecord.displayScore;
@@ -306,9 +297,7 @@ export const TrackerActiveCard = ({
const updateTrackerBind = (patch: Parameters<typeof requestManager.updateTrackerBind>[1]) => {
requestManager
.updateTrackerBind(trackRecord.id, patch)
.response.catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
.response.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
};
return (
@@ -320,7 +309,7 @@ export const TrackerActiveCard = ({
<Box sx={{ padding: 1 }}>
<TrackerActiveCardInfoRow>
<ListPreference
ListPreferenceTitle={t('manga.label.status')}
ListPreferenceTitle={t`Status`}
entries={tracker.statuses.map((status) => status.name)}
key="status"
type="ListPreference"
@@ -331,8 +320,8 @@ export const TrackerActiveCard = ({
/>
<Divider orientation="vertical" flexItem />
<NumberSetting
settingTitle={t('chapter.title_other')}
dialogTitle={t('chapter.title_other')}
settingTitle={t`Chapter`}
dialogTitle={t`Chapter`}
settingValue={`${trackRecord.lastChapterRead}/${trackRecord.totalChapters}`}
value={trackRecord.lastChapterRead}
minValue={0}
@@ -342,7 +331,7 @@ export const TrackerActiveCard = ({
/>
<Divider orientation="vertical" flexItem />
<SelectSetting<string>
settingName={t('tracking.track_record.label.score')}
settingName={t`Score`}
value={currentScore}
values={selectSettingValues}
handleChange={(score) => updateTrackerBind({ scoreString: score })}
@@ -351,7 +340,7 @@ export const TrackerActiveCard = ({
<Divider />
<TrackerActiveCardInfoRow>
<DateSetting
settingName={t('tracking.track_record.label.start_date')}
settingName={t`Start date`}
value={Trackers.getDateString(trackRecord.startDate)}
remove
handleChange={(startDate) =>
@@ -360,7 +349,7 @@ export const TrackerActiveCard = ({
/>
<Divider orientation="vertical" flexItem />
<DateSetting
settingName={t('tracking.track_record.label.finish_date')}
settingName={t`Finish date`}
value={Trackers.getDateString(trackRecord.finishDate)}
remove
handleChange={(finishDate) =>

View File

@@ -9,7 +9,6 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import CardActionArea from '@mui/material/CardActionArea';
import CardMedia from '@mui/material/CardMedia';
@@ -19,6 +18,7 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useLayoutEffect, useRef, useState } from 'react';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SpinnerImage } from '@/base/components/SpinnerImage.tsx';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
@@ -52,7 +52,7 @@ const TrackerMangaCardTitle = ({ title, selected }: { title: string; selected: b
const SUMMARY_COLLAPSED_SIZE = 50;
const TrackerMangaCardSummary = ({ summary }: { summary: string }) => {
const { t } = useTranslation();
const { t } = useLingui();
const summaryRef = useRef<HTMLParagraphElement>(null);
@@ -85,7 +85,7 @@ const TrackerMangaCardSummary = ({ summary }: { summary: string }) => {
setIsSummaryExpanded(!isSummaryExpanded);
}}
>
{t(isSummaryExpanded ? 'global.button.show_less' : 'global.button.show_more')}
{isSummaryExpanded ? t`Show less` : t`Show more`}
</Button>
)}
</>
@@ -115,7 +115,7 @@ export const TrackerMangaCard = ({
selected: boolean;
onSelect: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const isMobileWidth = MediaQuery.useIsMobileWidth();
return (
@@ -175,25 +175,19 @@ export const TrackerMangaCard = ({
</TrackerMangaCardLink>
{manga.publishingType && (
<Metadata
title={t('global.label.type')}
title={t`Type`}
value={t(PUBLISHING_TYPE_TO_TRANSLATION[Trackers.getPublishingType(manga)])}
/>
)}
{manga.startDate && (
<Metadata title={t('global.label.started')} value={manga.startDate} />
)}
{manga.startDate && <Metadata title={t`Started`} value={manga.startDate} />}
{manga.publishingStatus && (
<Metadata
title={t('manga.label.status')}
title={t`Status`}
value={t(PUBLISHING_STATUS_TO_TRANSLATION[Trackers.getPublishingStatus(manga)])}
/>
)}
{manga.score > 0 && (
<Metadata title={t('tracking.track_record.label.score')} value={manga.score} />
)}
{manga.totalChapters > 0 && (
<Metadata title={t('chapter.title_other')} value={manga.totalChapters} />
)}
{manga.score > 0 && <Metadata title={t`Score`} value={manga.score} />}
{manga.totalChapters > 0 && <Metadata title={t`Chapter`} value={manga.totalChapters} />}
</Stack>
</Stack>
<TrackerMangaCardSummary summary={manga.summary} />

View File

@@ -9,12 +9,12 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CARD_STYLING } from '@/features/tracker/Tracker.constants.ts';
import { TTrackerBase } from '@/features/tracker/Tracker.types.ts';
import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
export const TrackerUntrackedCard = ({
@@ -24,7 +24,7 @@ export const TrackerUntrackedCard = ({
tracker: Pick<TTrackerBase, 'name' | 'icon'>;
onClick: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Card sx={CARD_STYLING}>
@@ -49,7 +49,7 @@ export const TrackerUntrackedCard = ({
}}
/>
<Button sx={{ flexGrow: '1' }} onClick={onClick}>
{t('tracking.action.button.add_tracking')}
{t`Add tracking`}
</Button>
</Stack>
</CardContent>