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,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -14,6 +13,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useUpdateChecker } from '@/features/app-updates/hooks/useUpdateChecker.tsx';
import { VersionUpdateInfoDialog } from '@/features/app-updates/components/VersionUpdateInfoDialog.tsx';
@@ -24,7 +24,7 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
const disabledUpdateCheck = () => Promise.resolve();
export const ServerUpdateChecker = () => {
const { t } = useTranslation();
const { t } = useLingui();
const [serverVersion, setServerVersion] = useLocalStorage<string>('serverVersion');
const [open, setOpen] = useState(false);
@@ -98,11 +98,8 @@ export const ServerUpdateChecker = () => {
return (
<VersionUpdateInfoDialog
info={t('global.update.label.info', {
channel: selectedServerChannelInfo.channel,
version: selectedServerChannelInfo.tag,
})}
actionTitle={t('global.button.download')}
info={t`Server version ${selectedServerChannelInfo.tag} (${selectedServerChannelInfo.channel}) available for download`}
actionTitle={t`Download`}
actionUrl={selectedServerChannelInfo.url}
updateCheckerProps={['server', checkForUpdate, selectedServerChannelInfo?.tag]}
/>
@@ -115,20 +112,14 @@ export const ServerUpdateChecker = () => {
return (
<Dialog open={open}>
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
<DialogTitle>{t`Updated version`}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('global.update.label.update_success', {
name: t('settings.server.title.server'),
version,
channel: aboutServer?.buildType,
})}
</DialogContentText>
<DialogContentText>{t`Server was updated to version ${version} (${aboutServer?.buildType})`}</DialogContentText>
</DialogContent>
<DialogActions>
{changelogUrl && (
<Button href={changelogUrl} target="_blank" rel="noreferrer">
{t('global.button.changelog')}
{t`Changelog`}
</Button>
)}
<Button
@@ -138,7 +129,7 @@ export const ServerUpdateChecker = () => {
}}
variant="contained"
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -13,8 +13,8 @@ import Typography from '@mui/material/Typography';
import RefreshIcon from '@mui/icons-material/Refresh';
import DownloadIcon from '@mui/icons-material/Download';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { t as translate } from 'i18next';
import DownloadingIcon from '@mui/icons-material/Downloading';
import { t } from '@lingui/core/macro';
import { UpdateState } from '@/lib/graphql/generated/graphql.ts';
export type BaseVersionInfoProps = {
@@ -69,27 +69,27 @@ const getUpdateCheckButtonText = (
) => {
const isUpdating = updateState === UpdateState.Downloading;
if (isUpdating) {
return translate('global.update.label.updating', { progress });
return t`${progress}% | Updating…`;
}
const didUpdateFail = updateState === UpdateState.Error;
if (didUpdateFail) {
return translate('global.update.label.update_failure');
return t`Update failed`;
}
if (isLoading) {
return translate('global.update.label.checking');
return t`Checking for update`;
}
if (error) {
return translate('global.update.label.check_failure');
return t`Could not check for update`;
}
if (isUpdateAvailable) {
return translate('global.update.label.available');
return t`Update available`;
}
return translate('global.update.label.up_to_date');
return t`This is the latest version`;
};
export const VersionInfo = ({

View File

@@ -15,8 +15,8 @@ import DialogContentText from '@mui/material/DialogContentText';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
import { useLingui } from '@lingui/react/macro';
import { useUpdateChecker } from '@/features/app-updates/hooks/useUpdateChecker.tsx';
interface BaseProps {
@@ -48,7 +48,7 @@ export const VersionUpdateInfoDialog = ({
changelogUrl,
disabled,
}: VersionUpdateInfoDialogProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const updateChecker = useUpdateChecker(...updateCheckerProps);
@@ -58,7 +58,7 @@ export const VersionUpdateInfoDialog = ({
return (
<Dialog open>
<DialogTitle>{t('global.update.label.available')}</DialogTitle>
<DialogTitle>{t`Update available`}</DialogTitle>
<DialogContent>
<DialogContentText>{info}</DialogContentText>
</DialogContent>
@@ -72,7 +72,7 @@ export const VersionUpdateInfoDialog = ({
>
{changelogUrl && (
<Button href={changelogUrl} target="_blank" rel="noreferrer">
{t('global.button.changelog')}
{t`Changelog`}
</Button>
)}
<Stack direction="row">
@@ -80,7 +80,7 @@ export const VersionUpdateInfoDialog = ({
{(popupState) => (
<>
<Button disabled={disabled} {...bindTrigger(popupState)}>
{t('global.label.close')}
{t`Close`}
</Button>
<Menu {...bindMenu(popupState)}>
<MenuItem
@@ -89,7 +89,7 @@ export const VersionUpdateInfoDialog = ({
popupState.close();
}}
>
{t('global.button.remind_later')}
{t`Remind later`}
</MenuItem>
<MenuItem
onClick={() => {
@@ -97,7 +97,7 @@ export const VersionUpdateInfoDialog = ({
popupState.close();
}}
>
{t('global.button.ignore')}
{t`Ignore`}
</MenuItem>
</Menu>
</>

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -14,6 +13,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { UpdateState, WebUiChannel, WebUiUpdateStatus } from '@/lib/graphql/generated/graphql.ts';
import { useLocalStorage, useSessionStorage } from '@/base/hooks/useStorage.tsx';
@@ -36,7 +36,7 @@ if (BrowserUtil.isActualPageLoad()) {
}
export const WebUIUpdateChecker = () => {
const { t } = useTranslation();
const { t } = useLingui();
const [webUIVersion, setWebUIVersion] = useLocalStorage<string>('webUIVersion');
const [initialLoadTimestamp] = useSessionStorage<number>(INITIAL_LOAD_TIMESTAMP_KEY, Date.now());
@@ -93,7 +93,7 @@ export const WebUIUpdateChecker = () => {
useEffect(() => {
const isError = webUIUpdateState === UpdateState.Error;
if (isError) {
makeToast(t('settings.about.webui.label.update_failure'), 'error');
makeToast(t`Could not update WebUI`, 'error');
}
const updateFinished = webUIUpdateState === UpdateState.Finished;
@@ -139,24 +139,15 @@ export const WebUIUpdateChecker = () => {
return (
<VersionUpdateInfoDialog
info={t('settings.about.webui.label.info', {
version: webUIUpdateData?.checkForWebUIUpdate.tag,
channel: webUIUpdateData?.checkForWebUIUpdate.channel,
})}
info={t`WebUI version ${webUIUpdateData?.checkForWebUIUpdate.tag} (${webUIUpdateData?.checkForWebUIUpdate.channel}) available for download`}
changelogUrl={changelogUrl}
disabled={isUpdateInProgress}
onAction={() =>
requestManager
.updateWebUI()
.response.catch((e) =>
makeToast(t('settings.about.webui.label.update_failure'), 'error', getErrorMessage(e)),
)
}
actionTitle={
isUpdateInProgress
? t('global.update.label.updating', { progress: updateStatus.progress })
: t('extension.action.label.update')
.response.catch((e) => makeToast(t`Could not update WebUI`, 'error', getErrorMessage(e)))
}
actionTitle={isUpdateInProgress ? t`${updateStatus.progress}% | Updating…` : t`Update`}
updateCheckerProps={[
'webUI',
isAutoUpdateEnabled ? disabledUpdateCheck : checkForUpdate,
@@ -172,19 +163,13 @@ export const WebUIUpdateChecker = () => {
return (
<Dialog open={open} onClose={shouldForceRefresh ? () => setOpen(false) : noOp}>
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
<DialogTitle>{t`Updated version`}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('global.update.label.update_success', {
name: t('settings.webui.title.webui'),
version: newVersion,
channel: aboutWebUI?.channel,
})}
</DialogContentText>
<DialogContentText>{t`{name} was updated to version ${newVersion} (${aboutWebUI?.channel})`}</DialogContentText>
</DialogContent>
<DialogActions>
<Button href={changelogUrl} target="_blank" rel="noreferrer">
{t('global.button.changelog')}
{t`Changelog`}
</Button>
<Button
onClick={() => {
@@ -197,7 +182,7 @@ export const WebUIUpdateChecker = () => {
}}
variant="contained"
>
{t(shouldForceRefresh ? 'global.button.refresh' : 'global.button.ok')}
{shouldForceRefresh ? t`Refresh` : t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
@@ -14,6 +13,7 @@ import Button from '@mui/material/Button';
import { Navigate, useNavigate } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import { StringParam, useQueryParam } from 'use-query-params';
import { useLingui } from '@lingui/react/macro';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
@@ -27,7 +27,7 @@ import { ServerAddressSetting } from '@/features/settings/components/ServerAddre
export const LoginPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const { t } = useLingui();
const { setOverride } = useNavBarContext();
const navigate = useNavigate();
const isAuthenticated = AuthManager.useIsAuthenticated();
@@ -48,7 +48,7 @@ export const LoginPage = () => {
navigate(redirect ?? AppRoutes.root.path);
}
} catch (e) {
makeToast(t('tracking.action.login.label.failure', { name: 'Suwayomi' }), 'error', getErrorMessage(e));
makeToast(t`Could not log in to Suwayomi`, 'error', getErrorMessage(e));
}
};
@@ -113,7 +113,7 @@ export const LoginPage = () => {
margin="dense"
id="username"
name="username"
label={t('global.label.username')}
label={t`Username`}
type="text"
fullWidth
variant="standard"
@@ -127,7 +127,7 @@ export const LoginPage = () => {
/>
</Stack>
<Button disabled={isLoading || (!username && !password)} variant="contained" onClick={doLogin}>
{t('global.button.log_in')}
{t`Log in`}
</Button>
<Stack sx={{ position: 'absolute', left: 0, bottom: 0 }}>
<ServerAddressSetting />

View File

@@ -6,22 +6,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { BackupFlag, BackupFlagGroup } from '@/features/backup/Backup.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, TranslationKey> = {
includeManga: 'settings.backup.flag.manga',
includeCategories: 'settings.backup.flag.categories',
includeChapters: 'settings.backup.flag.chapters',
includeClientData: 'settings.backup.flag.client_data',
includeHistory: 'settings.backup.flag.history',
includeServerSettings: 'settings.backup.flag.server_settings',
includeTracking: 'settings.backup.flag.tracking',
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, MessageDescriptor> = {
includeManga: msg`Library entries`,
includeCategories: msg`Categories`,
includeChapters: msg`Chapters`,
includeClientData: msg`Client data`,
includeHistory: msg`History`,
includeServerSettings: msg`Server settings`,
includeTracking: msg`Tracking`,
};
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, TranslationKey> = {
[BackupFlagGroup.LIBRARY]: 'settings.backup.flag.group.library',
[BackupFlagGroup.SETTINGS]: 'settings.backup.flag.group.settings',
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, MessageDescriptor> = {
[BackupFlagGroup.LIBRARY]: msg`Library`,
[BackupFlagGroup.SETTINGS]: msg`Settings`,
};
export const BACKUP_FLAGS = Object.keys(BACKUP_FLAGS_TO_TRANSLATION) as readonly BackupFlag[];

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t as translate } from 'i18next';
import { plural, t } from '@lingui/core/macro';
import { AutoBackupFlagInclusionState, BackupFlag, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
import { BACKUP_FLAGS_TO_TRANSLATION } from '@/features/backup/Backup.constants.ts';
@@ -32,11 +32,11 @@ export const convertToBackupFlags = (flags: AutoBackupFlagInclusionState): Backu
const getIncludeExcludeText = (count: number, allCount: number, specificText: string): string => {
if (count === 0) {
return translate('global.label.none');
return t`None`;
}
if (count === allCount) {
return translate('extension.language.all');
return t`All`;
}
return specificText;
@@ -49,9 +49,9 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
const flagsByState = Object.groupBy(Object.entries(flags), ([, value]) => value.toString());
const includedFlagsString =
flagsByState.true?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
flagsByState.true?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
const excludedFlagsString =
flagsByState.false?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
flagsByState.false?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
return {
false: getIncludeExcludeText(flagsByState.false?.length ?? 0, totalFlags, excludedFlagsString),
@@ -61,8 +61,11 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
export const getBackupCleanupDisplayValue = (ttl: number): string => {
if (ttl === 0) {
return translate('global.label.never');
return t`Never`;
}
return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl });
return plural(ttl, {
one: `Delete backups that are older than # day`,
other: `Delete backups that are older than # days`,
});
};

View File

@@ -11,12 +11,12 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import FormGroup from '@mui/material/FormGroup';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { AwaitableComponentProps } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import {
BACKUP_FLAG_GROUP_TO_TRANSLATION,
@@ -34,7 +34,7 @@ export const BackupFlagInclusionDialog = ({
title,
flags,
}: AwaitableComponentProps<BackupFlagInclusionState> & { title: string; flags?: BackupFlagInclusionState }) => {
const { t } = useTranslation();
const { t } = useLingui();
const [includeStateByFlag, setIncludeStateByFlag] = useState(
Object.fromEntries(BACKUP_FLAGS.map((flag) => [flag, flags?.[flag] ?? true])) as BackupFlagInclusionState,
@@ -67,10 +67,10 @@ export const BackupFlagInclusionDialog = ({
</DialogContent>
<DialogActions>
<Button autoFocus onClick={onDismiss} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={() => onSubmit(includeStateByFlag)} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -7,7 +7,6 @@
*/
import List from '@mui/material/List';
import { useTranslation } from 'react-i18next';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
@@ -17,6 +16,7 @@ import ListItem from '@mui/material/ListItem';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import { AwaitableComponentProps } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { BrowseTab } from '@/features/browse/Browse.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { ValidateBackupResult } from '@/lib/graphql/generated/graphql.ts';
@@ -28,16 +28,16 @@ export const BackupValidationDialog = ({
isVisible,
onExitComplete,
}: AwaitableComponentProps & { validationResult: ValidateBackupResult }) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Dialog open={isVisible} onTransitionExited={onExitComplete} onClose={onDismiss}>
<DialogTitle>{t('settings.backup.action.validate.dialog.title')}</DialogTitle>
<DialogTitle>{t`Backup validation`}</DialogTitle>
<DialogContent dividers>
{!!validationResult?.missingSources.length && (
<List
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
subheader={t('settings.backup.action.validate.dialog.content.label.missing_sources')}
subheader={t`The following sources are not installed:`}
>
{validationResult?.missingSources.map(({ id, name }) => (
<ListItem sx={{ display: 'list-item' }} key={id}>
@@ -49,7 +49,7 @@ export const BackupValidationDialog = ({
{!!validationResult?.missingTrackers.length && (
<List
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
subheader={t('settings.backup.action.validate.dialog.content.label.missing_trackers')}
subheader={t`The following trackers are not logged in:`}
>
{validationResult?.missingTrackers.map(({ name }) => (
<ListItem sx={{ display: 'list-item' }} key={name}>
@@ -75,7 +75,7 @@ export const BackupValidationDialog = ({
autoFocus={!!validationResult?.missingSources.length}
variant={validationResult?.missingSources.length ? 'contained' : 'text'}
>
{t('extension.action.label.install')}
{t`Install`}
</Button>
)}
{!!validationResult?.missingTrackers.length && (
@@ -86,11 +86,11 @@ export const BackupValidationDialog = ({
autoFocus={!!validationResult?.missingTrackers.length}
variant={validationResult?.missingTrackers.length ? 'contained' : 'text'}
>
{t('global.button.log_in')}
{t`Log in`}
</Button>
)}
<Stack direction="row">
<Button onClick={onDismiss}>{t('global.button.cancel')}</Button>
<Button onClick={onDismiss}>{t`Cancel`}</Button>
<Button
onClick={onSubmit}
autoFocus={
@@ -102,7 +102,7 @@ export const BackupValidationDialog = ({
: 'text'
}
>
{t('global.button.restore')}
{t`Restore`}
</Button>
</Stack>
</Stack>

View File

@@ -10,12 +10,13 @@ import { useEffect, useRef, useState } from 'react';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import { fromEvent } from 'file-selector';
import { useTranslation } from 'react-i18next';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListSubheader from '@mui/material/ListSubheader';
import { useEventListener, useMergedRef, useWindowEvent } from '@mantine/hooks';
import { AwaitableComponent } from 'awaitable-component';
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 { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts';
@@ -41,9 +42,9 @@ import { BackupSettingsType } from '@/features/backup/Backup.types.ts';
let backupRestoreId: string | undefined;
export function Backup() {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.backup.title'));
useAppTitle(t`Backup`);
const {
data: settingsData,
@@ -76,7 +77,7 @@ export function Backup() {
value: BackupSettingsType[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -84,7 +85,7 @@ export function Backup() {
settings: Record<Setting, BackupSettingsType[Setting]>,
) => {
mutateSettings({ variables: { input: { settings } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -99,11 +100,11 @@ export function Backup() {
const isRestoreFinished = isSuccess || isFailure;
if (isRestoreFinished) {
if (isSuccess) {
makeToast(t('settings.backup.action.restore.label.success'), 'success');
makeToast(t`Backup restored.`, 'success');
}
if (isFailure) {
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error');
makeToast(t`Could not restore backup`, 'error');
}
requestManager.reset();
@@ -121,21 +122,17 @@ export function Backup() {
const createBackup = async () => {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t('settings.backup.action.create.label.title'),
title: t`Create backup`,
});
makeToast(t('settings.backup.action.create.label.in_progress'), 'info');
makeToast(t`Creating backup`, 'info');
try {
const backupFileResponse = await requestManager.createBackupFile({ flags }).response;
const backupFileUrl = backupFileResponse.data?.createBackup.url;
if (!backupFileUrl) {
makeToast(
t('settings.backup.action.create.error.failure'),
'error',
getErrorMessage(backupFileResponse.errors),
);
makeToast(t`Could not create backup`, 'error', getErrorMessage(backupFileResponse.errors));
return;
}
@@ -146,7 +143,7 @@ export function Backup() {
link.click();
document.body.removeChild(link);
} catch (e) {
makeToast(t('settings.backup.action.create.error.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not create backup`, 'error', getErrorMessage(e));
}
};
@@ -172,7 +169,7 @@ export function Backup() {
return true;
} catch (e) {
makeToast(t('settings.backup.action.validate.error.label.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not validate backup`, 'error', getErrorMessage(e));
} finally {
resetBackupState();
}
@@ -182,17 +179,17 @@ export function Backup() {
const restoreBackup = async (backup: File) => {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t('settings.backup.action.restore.label.title'),
title: t`Restore Backup`,
});
try {
makeToast(t('settings.backup.action.restore.label.in_progress'), 'info');
makeToast(t`Restoring backup`, 'info');
const response = await requestManager.restoreBackupFile({ backup, flags }).response;
backupRestoreId = response.data?.restoreBackup.id;
setTriggerReRender(Date.now());
} catch (e) {
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not restore backup`, 'error', getErrorMessage(e));
} finally {
resetBackupState();
}
@@ -200,13 +197,13 @@ export function Backup() {
const submitBackup = async (file: File) => {
if (file.name.toLowerCase().endsWith('json')) {
makeToast(t('settings.backup.action.restore.error.label.legacy_backup_unsupported'), 'error');
makeToast(t`legacy backups are not supported!`, 'error');
return;
}
const isValidFilename = file.name.toLowerCase().match(/proto\.gz$|tachibk$/g);
if (!isValidFilename) {
makeToast(t('global.error.label.invalid_file_type'), 'error');
makeToast(t`Invalid filetype`, 'error');
return;
}
@@ -238,7 +235,7 @@ export function Backup() {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('Backup::refetch'))}
/>
@@ -247,19 +244,20 @@ export function Backup() {
const backupSettings = settingsData!.settings;
const autoBackupFlagsInfo = getAutoBackupFlagsInfo(backupSettings);
const includedCategoriesText = autoBackupFlagsInfo.true;
const excludedCategoriesText = autoBackupFlagsInfo.false;
return (
<>
<List sx={{ padding: 0 }}>
<ListItemButton onClick={createBackup}>
<ListItemText
primary={t('settings.backup.action.create.label.title')}
secondary={t('settings.backup.action.create.label.description')}
/>
<ListItemText primary={t`Create backup`} secondary={t`Back up library as a Tachiyomi backup`} />
</ListItemButton>
<ListItemButton onClick={() => inputRef.current?.click()} disabled={!!backupRestoreId}>
<ListItemText
primary={t('settings.backup.action.restore.label.title')}
secondary={t('settings.backup.action.restore.label.description')}
primary={t`Restore Backup`}
secondary={t`You can also drag and drop the backup file here to restore it`}
/>
{backupRestoreId ? (
<ListItemIcon>
@@ -275,19 +273,17 @@ export function Backup() {
}
>
<TextSetting
settingName={t('settings.backup.automated.location.label.title')}
dialogDescription={t('settings.backup.automated.location.label.description')}
settingName={t`Backup location`}
dialogDescription={t`The path to the directory on the server where automated backups should get saved in`}
value={backupSettings.backupPath}
settingDescription={
backupSettings.backupPath.length ? backupSettings.backupPath : t('global.label.default')
}
settingDescription={backupSettings.backupPath.length ? backupSettings.backupPath : t`Default`}
handleChange={(path) => updateSetting('backupPath', path)}
/>
<ListItemButton
onClick={async () => {
try {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t('settings.backup.automated.flags.title'),
title: t`Backup data`,
flags: convertToBackupFlags(backupSettings),
});
@@ -298,54 +294,46 @@ export function Backup() {
}}
>
<ListItemText
primary={t('settings.backup.automated.flags.title')}
primary={t`Backup data`}
secondary={
<>
<span>
{t('category.settings.inclusion.label.include', {
includedCategoriesText: getAutoBackupFlagsInfo(backupSettings).true,
})}
</span>
<span>
{t('category.settings.inclusion.label.exclude', {
excludedCategoriesText: getAutoBackupFlagsInfo(backupSettings).false,
})}
</span>
<span>{t`Include: ${includedCategoriesText}`}</span>
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
</>
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<TimeSetting
settingName={t('settings.backup.automated.label.time')}
settingName={t`Backup time`}
value={backupSettings.backupTime}
defaultValue="00:00"
handleChange={(time: string) => updateSetting('backupTime', time)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.label.interval')}
settingValue={t('global.date.value.label.day', {
days: backupSettings.backupInterval,
count: backupSettings.backupInterval,
settingTitle={t`Backup interval`}
settingValue={plural(backupSettings.backupInterval, {
one: '# day',
other: '# days',
})}
value={backupSettings.backupInterval}
defaultValue={1}
minValue={1}
maxValue={31}
stepSize={1}
valueUnit={t('global.date.label.day_one')}
valueUnit={t`Day`}
showSlider
handleUpdate={(interval: number) => updateSetting('backupInterval', interval)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.cleanup.label.title')}
settingTitle={t`Backup cleanup`}
settingValue={getBackupCleanupDisplayValue(backupSettings.backupTTL)}
value={backupSettings.backupTTL}
defaultValue={14}
minValue={0}
maxValue={1000}
stepSize={1}
valueUnit={t('global.date.label.day_one')}
valueUnit={t`Day`}
showSlider
handleUpdate={(ttl: number) => updateSetting('backupTTL', ttl)}
/>

View File

@@ -14,9 +14,9 @@ import { StringParam, useQueryParam } from 'use-query-params';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useWindowEvent } from '@mantine/hooks';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
@@ -42,7 +42,7 @@ import {
ExtensionState,
TExtension,
} from '@/features/extension/Extensions.types.ts';
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts';
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP } from '@/features/extension/Extensions.constants.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import {
@@ -52,6 +52,7 @@ import {
import { MetadataBrowseSettings } from '@/features/browse/Browse.types.ts';
import { useAppAction } from '@/features/navigation-bar/hooks/useAppAction.ts';
import { SearchParam } from '@/base/Base.types.ts';
import { i18n } from '@/i18n';
const LANGUAGE = 0;
const EXTENSIONS = 1;
@@ -73,7 +74,7 @@ const GroupHeader = ({
setUpdatingExtensionIds: (ids: TExtension['pkgName'][]) => void;
handleExtensionUpdate: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<StyledGroupHeader
@@ -96,8 +97,10 @@ const GroupHeader = ({
.response.then(() => handleExtensionUpdate())
.catch((e) =>
makeToast(
t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[ExtensionAction.UPDATE], {
count: groupExtensionIds.length,
/* lingui-extract-ignore */
i18n.t({
...EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP[ExtensionAction.UPDATE],
values: { count: groupExtensionIds.length },
}),
'error',
getErrorMessage(e),
@@ -106,7 +109,7 @@ const GroupHeader = ({
.finally(() => setUpdatingExtensionIds([]));
}}
>
{t('extension.action.label.update_all')}
{t`Update all`}
</Button>
)}
</StyledGroupHeader>
@@ -114,7 +117,7 @@ const GroupHeader = ({
};
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useTranslation();
const { t } = useLingui();
const {
data: serverSettingsData,
@@ -130,7 +133,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<
keyof Pick<MetadataBrowseSettings, 'extensionLanguages'>
>((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
>((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
const [query] = useQueryParam(SearchParam.QUERY, StringParam);
@@ -170,18 +173,18 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const submitExternalExtension = (file: File) => {
if (!file.name.toLowerCase().endsWith('apk')) {
makeToast(t('global.error.label.invalid_file_type'), 'error');
makeToast(t`Invalid filetype`, 'error');
return;
}
makeToast(t('extension.label.installing_file'), 'info');
makeToast(t`Installing extension file…`, 'info');
requestManager
.installExternalExtension(file)
.response.then(() => {
handleExtensionUpdate();
makeToast(t('extension.label.installed_successfully'), 'success');
makeToast(t`Extension installed`, 'success');
})
.catch((e) => makeToast(t('extension.label.installation_failed'), 'error', getErrorMessage(e)));
.catch((e) => makeToast(t`Could not install the extension`, 'error', getErrorMessage(e)));
};
useEffect(() => {
@@ -191,7 +194,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
useAppAction(
<>
<AppbarSearch />
<CustomTooltip title={t('extension.action.label.install_external')}>
<CustomTooltip title={t`Install external extension`}>
<IconButton
onClick={() => {
const input = document.createElement('input');
@@ -241,7 +244,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (serverSettingsError) {
@@ -267,9 +270,9 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
paddingTop: '20px',
}}
>
<Typography>{t('extension.label.add_repository_info')}</Typography>
<Typography>{t`You have to add a extension repository to be able to install extensions`}</Typography>
<Button component={Link} variant="contained" to={AppRoutes.settings.childRoutes.browse.path}>
{t('settings.title')}
{t`Settings`}
</Button>
</Stack>
);

View File

@@ -10,10 +10,10 @@ import { useEffect, useState } from 'react';
import Card from '@mui/material/Card';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import Stack from '@mui/material/Stack';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import {
@@ -26,7 +26,7 @@ import {
import {
EXTENSION_ACTION_TO_NEXT_ACTION_MAP,
EXTENSION_ACTION_TO_STATE_MAP,
INSTALLED_STATE_TO_TRANSLATION_KEY_MAP,
INSTALLED_STATE_TO_TRANSLATION_MAP,
} from '@/features/extension/Extensions.constants.ts';
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
@@ -45,7 +45,7 @@ interface IProps {
}
export function ExtensionCard(props: IProps) {
const { t } = useTranslation();
const { t } = useLingui();
const {
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo },
@@ -134,7 +134,7 @@ export function ExtensionCard(props: IProps) {
{showSourceRepo && <Typography variant="caption">{repo}</Typography>}
</Stack>
{isInstalled && (
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton color="inherit" {...MUIUtil.preventRippleProp()}>
<SettingsIcon />
</IconButton>
@@ -151,7 +151,7 @@ export function ExtensionCard(props: IProps) {
handleButtonClick();
}}
>
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
{t(INSTALLED_STATE_TO_TRANSLATION_MAP[installedState])}
</Button>
</ListCardContent>
</OptionalCardActionAreaLink>

View File

@@ -8,8 +8,8 @@
import { useCallback, useRef, useState } from 'react';
import Tab from '@mui/material/Tab';
import { useTranslation } from 'react-i18next';
import { StringParam, useQueryParam } from 'use-query-params';
import { useLingui } from '@lingui/react/macro';
import { Sources } from '@/features/browse/sources/Sources.tsx';
import { Extensions } from '@/features/browse/extensions/Extensions.tsx';
import { TabPanel } from '@/base/components/tabs/TabPanel.tsx';
@@ -23,8 +23,8 @@ import { GROUPED_VIRTUOSO_Z_INDEX } from '@/lib/virtuoso/Virtuoso.constants.ts';
import { SearchParam } from '@/base/Base.types.ts';
export function Browse() {
const { t } = useTranslation();
useAppTitle(t('global.label.browse'));
const { t } = useLingui();
useAppTitle(t`Browse`);
const tabsMenuRef = useRef<HTMLDivElement | null>(null);
const [tabsMenuHeight, setTabsMenuHeight] = useState(0);
@@ -49,9 +49,9 @@ export function Browse() {
value={tabName}
onChange={(_, newTab) => setTabSearchParam(newTab, 'replaceIn')}
>
<Tab value={BrowseTab.SOURCES} sx={{ textTransform: 'none' }} label={t('source.title_other')} />
<Tab value={BrowseTab.EXTENSIONS} sx={{ textTransform: 'none' }} label={t('extension.title_other')} />
<Tab value={BrowseTab.MIGRATE} sx={{ textTransform: 'none' }} label={t('migrate.title')} />
<Tab value={BrowseTab.SOURCES} sx={{ textTransform: 'none' }} label={t`Source`} />
<Tab value={BrowseTab.EXTENSIONS} sx={{ textTransform: 'none' }} label={t`Extension`} />
<Tab value={BrowseTab.MIGRATE} sx={{ textTransform: 'none' }} label={t`Migrate`} />
</TabsMenu>
<TabPanel index={BrowseTab.SOURCE_DEPRECATED} currentIndex={tabName}>
<Sources tabsMenuHeight={tabsMenuHeight} />

View File

@@ -6,11 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Trans, useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { Trans, useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { MutableListSetting } from '@/base/components/settings/MutableListSetting.tsx';
@@ -31,9 +32,9 @@ import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
type ExtensionsSettings = Pick<GqlServerSettings, 'maxSourcesInParallel' | 'localSourcePath' | 'extensionRepos'>;
export const BrowseSettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('global.label.browse'));
useAppTitle(t`Browse`);
const { data, loading, error, refetch } = requestManager.useGetServerSettings({
notifyOnNetworkStatusChange: true,
@@ -45,7 +46,7 @@ export const BrowseSettings = () => {
value: ExtensionsSettings[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -53,7 +54,7 @@ export const BrowseSettings = () => {
settings: { hideLibraryEntries, showNsfw },
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<keyof MetadataBrowseSettings>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
if (loading) {
@@ -63,7 +64,7 @@ export const BrowseSettings = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('BrowseSettings::refetch'))}
/>
@@ -75,7 +76,7 @@ export const BrowseSettings = () => {
return (
<List sx={{ pt: 0 }}>
<ListItem>
<ListItemText primary={t('settings.label.hide_library_entries')} />
<ListItemText primary={t`Hide entries already in library`} />
<Switch
edge="end"
checked={hideLibraryEntries}
@@ -83,10 +84,7 @@ export const BrowseSettings = () => {
/>
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.label.show_nsfw')}
secondary={t('settings.label.show_nsfw_description')}
/>
<ListItemText primary={t`Show NSFW`} secondary={t`Hide NSFW extensions and sources`} />
<Switch
edge="end"
checked={showNsfw}
@@ -94,12 +92,12 @@ export const BrowseSettings = () => {
/>
</ListItem>
<NumberSetting
settingTitle={t('settings.server.requests.sources.parallel.label.title')}
settingValue={t('settings.server.requests.sources.parallel.label.value', {
value: serverSettings.maxSourcesInParallel,
count: serverSettings.maxSourcesInParallel,
settingTitle={t`Parallel source requests`}
settingValue={plural(serverSettings.maxSourcesInParallel, {
one: '# Source',
other: '# Sources',
})}
valueUnit={t('source.title_one')}
valueUnit={t`Source`}
value={serverSettings.maxSourcesInParallel}
defaultValue={6}
minValue={1}
@@ -109,10 +107,10 @@ export const BrowseSettings = () => {
handleUpdate={(parallelSources) => updateSetting('maxSourcesInParallel', parallelSources)}
/>
<MutableListSetting
settingName={t('extension.settings.repositories.custom.label.title')}
description={t('extension.settings.repositories.custom.label.description')}
settingName={t`Extension repositories`}
description={t`Add repositories from which extensions can be installed`}
dialogDisclaimer={
<Trans i18nKey="extension.settings.repositories.custom.label.disclaimer">
<Trans>
<strong>Suwayomi does not provide any support for 3rd party repositories or extensions!</strong>
<br />
Use with caution as there could be malicious actors making those repositories.
@@ -125,22 +123,20 @@ export const BrowseSettings = () => {
requestManager.clearExtensionCache();
}}
valueInfos={serverSettings.extensionRepos.map((extensionRepo) => [extensionRepo])}
addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')}
addItemButtonTitle={t`Add repository`}
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
validateItem={(repo) =>
!!repo.match(
/https:\/\/(www\.|raw\.)?(github|githubusercontent)\.com\/([^/]+)\/([^/]+)((\/tree|\/blob)?\/([^/\n]*))?(\/([^/\n]*\.json)?)?/g,
)
}
invalidItemError={t('extension.settings.repositories.custom.error.label.invalid_url')}
invalidItemError={t`Invalid repository url`}
/>
<TextSetting
settingName={t('settings.server.local_source.path.label.title')}
dialogDescription={t('settings.server.local_source.path.label.description')}
settingName={t`Local source location`}
dialogDescription={t`The path to the directory on the server where local source files are saved in`}
value={serverSettings.localSourcePath}
settingDescription={
serverSettings.localSourcePath.length ? serverSettings.localSourcePath : t('global.label.default')
}
settingDescription={serverSettings.localSourcePath.length ? serverSettings.localSourcePath : t`Default`}
handleChange={(path) => updateSetting('localSourcePath', path)}
/>
</List>

View File

@@ -10,8 +10,8 @@ import { useCallback, useMemo } from 'react';
import IconButton from '@mui/material/IconButton';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DefaultLanguage } from '@/base/utils/Languages.ts';
@@ -32,7 +32,7 @@ import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupIt
import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx';
export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useTranslation();
const { t } = useLingui();
const { languages: shownLangs, setLanguages: setShownLangs } = SourceService.useLanguages();
const {
@@ -97,7 +97,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
useAppAction(
<>
<CustomTooltip title={t('search.title.global_search')}>
<CustomTooltip title={t`Global Search`}>
<IconButton onClick={() => navigate(AppRoutes.sources.childRoutes.searchAll.path())} color="inherit">
<TravelExploreIcon />
</IconButton>
@@ -117,7 +117,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('Sources::refetch'))}
/>
@@ -125,7 +125,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
}
if (sources?.length === 0) {
return <EmptyViewAbsoluteCentered message={t('source.error.label.no_sources_found')} />;
return <EmptyViewAbsoluteCentered message={t`No sources found. Install Some extensions first.`} />;
}
return (

View File

@@ -11,12 +11,12 @@ import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import Typography from '@mui/material/Typography';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import PushPinIcon from '@mui/icons-material/PushPin';
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
import IconButton from '@mui/material/IconButton';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { SourceContentType } from '@/features/source/browse/screens/SourceMangas.tsx';
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
@@ -38,7 +38,7 @@ interface IProps {
}
export const SourceCard: React.FC<IProps> = (props: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const { source, showSourceRepo, showLanguage } = props;
const {
@@ -53,10 +53,10 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
const { isPinned } = useGetSourceMetadata(source);
const sourceName = Sources.isLocalSource(source) ? t('source.local_source.title') : name;
const sourceName = Sources.isLocalSource(source) ? t`Local source` : name;
const updateSetting = createUpdateSourceMetadata(source, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
return (
@@ -105,10 +105,10 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
to={AppRoutes.sources.childRoutes.browse.path(id)}
state={{ contentType: SourceContentType.LATEST, clearCache: true }}
>
{t('global.button.latest')}
{t`Latest`}
</Button>
)}
<CustomTooltip title={t(isPinned ? 'source.pin.remove' : 'source.pin.add')}>
<CustomTooltip title={isPinned ? t`Unpin source` : t`Pin source`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
@@ -16,7 +15,8 @@ import DialogTitle from '@mui/material/DialogTitle';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { t as translate } from 'i18next';
import { useLingui } from '@lingui/react/macro';
import { t as translate } from '@lingui/core/macro';
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
@@ -68,11 +68,11 @@ const getCategoryUpdateInfo = (
const noSpecificallyIncludedCategories = areIncluded && !categories.length && unsetCategories;
const includesAllCategories = categories.length === allCategories;
if (noSpecificallyIncludedCategories || includesAllCategories) {
return translate('extension.language.all');
return translate`All`;
}
if (!categories.length) {
return translate('global.label.none');
return translate`None`;
}
return categories.map((category) => category.name).join(', ');
@@ -91,7 +91,7 @@ export const CategoriesInclusionSetting = ({
includeField,
dialogText,
}: CategoriesInclusionSettingProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [dialogCategories, setDialogCategories] = useState(categories);
const [isDialogOpen, setIsDialogOpen] = useState(false);
@@ -141,7 +141,7 @@ export const CategoriesInclusionSetting = ({
// TODO - update cache immediately
// mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false });
} catch (e) {
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e));
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e));
// mutate(categoriesEndpoint, [...categories]);
}
};
@@ -155,27 +155,18 @@ export const CategoriesInclusionSetting = ({
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('category.title.category_other')}
primary={t`Category`}
secondary={
<>
<span>
{t('category.settings.inclusion.label.include', {
includedCategoriesText,
})}
</span>
<span>
{t('category.settings.inclusion.label.exclude', {
excludedCategoriesText,
})}
</span>
<span>{t`Include: ${includedCategoriesText}`}</span>
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
</>
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{t('category.title.category_other')}</DialogTitle>
<DialogTitle>{t`Category`}</DialogTitle>
<DialogContent>
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
<CheckboxContainer>
@@ -207,10 +198,10 @@ export const CategoriesInclusionSetting = ({
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={updateCategories} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -13,10 +13,10 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import FormGroup from '@mui/material/FormGroup';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import { AwaitableComponentProps } from 'awaitable-component';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts';
@@ -90,7 +90,7 @@ const getCategoryCheckedState = (
};
export function CategorySelect(props: CategorySelectProps) {
const { t } = useTranslation();
const { t } = useLingui();
const {
onDismiss,
@@ -161,7 +161,7 @@ export function CategorySelect(props: CategorySelectProps) {
if (doNotShowAddToLibraryDialogAgain) {
updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch((e) =>
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
);
}
@@ -196,10 +196,10 @@ export function CategorySelect(props: CategorySelectProps) {
onTransitionExited={onExitComplete}
onClose={handleCancel}
>
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
<DialogTitle>{t`Set categories`}</DialogTitle>
<DialogContent dividers>
<FormGroup>
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
{allCategories.length === 0 && <span>{t`You don't have any categories yet.`}</span>}
{allCategories.map((category) => (
<ThreeStateCheckboxInput
checked={getCategoryCheckedState(
@@ -232,7 +232,7 @@ export function CategorySelect(props: CategorySelectProps) {
<CheckboxInput
sx={{ margin: 0 }}
size="small"
label={t('global.button.dont_show_dialog_again')}
label={t`Don't show this dialog again`}
onChange={(e) => setDoNotShowAddToLibraryDialogAgain(e.target.checked)}
/>
)}
@@ -249,15 +249,15 @@ export function CategorySelect(props: CategorySelectProps) {
to={AppRoutes.settings.childRoutes.categories.path}
onClick={onDismiss}
>
{t(allCategories.length ? 'global.button.edit' : 'global.button.create')}
{allCategories.length ? t`Edit` : t`Create`}
</Button>
<Stack direction="row">
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
{!!allCategories.length && (
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
)}
</Stack>

View File

@@ -10,11 +10,11 @@ import IconButton from '@mui/material/IconButton';
import DragHandleIcon from '@mui/icons-material/DragHandle';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CategoryType } from '@/lib/graphql/generated/graphql.ts';
@@ -27,7 +27,7 @@ export const CategorySettingsCard = ({
category: Pick<CategoryType, 'id' | 'name'>;
onEdit: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const deleteCategory = () => {
requestManager.deleteCategory(category.id);
@@ -42,12 +42,12 @@ export const CategorySettingsCard = ({
{category.name}
</Typography>
<Stack sx={{ flexDirection: 'row' }}>
<CustomTooltip title={t('global.button.edit')}>
<CustomTooltip title={t`Edit`}>
<IconButton component={Box} onClick={onEdit}>
<EditIcon />
</IconButton>
</CustomTooltip>
<CustomTooltip title={t('chapter.action.download.delete.label.action')}>
<CustomTooltip title={t`Delete`}>
<IconButton component={Box} onClick={deleteCategory}>
<DeleteIcon />
</IconButton>

View File

@@ -15,7 +15,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CategoryDefaultInfo, CategoryIdInfo, CategoryNameInfo } from '@/features/category/Category.types.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
@@ -31,7 +31,7 @@ export const CreateOrEditCategoryDialog = ({
}) => {
const isEditMode = !!category;
const { t } = useTranslation();
const { t } = useLingui();
const [dialogName, setDialogName] = useState(category?.name);
const [dialogDefault, setDialogDefault] = useState(!!category?.default);
@@ -47,47 +47,43 @@ export const CreateOrEditCategoryDialog = ({
if (isEditMode) {
requestManager
.updateCategory(category.id, { name: dialogName, default: dialogDefault })
.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;
}
requestManager
.createCategory({ name: dialogName, default: dialogDefault })
.response.catch((e) => makeToast(t('category.error.label.create_failure'), 'error', getErrorMessage(e)));
.response.catch((e) => makeToast(t`Could not create category`, 'error', getErrorMessage(e)));
};
return (
<Dialog open onClose={onClose}>
<DialogTitle id="form-dialog-title">
{isEditMode ? t('category.dialog.title.edit_category_one') : t('category.dialog.title.new_category')}
</DialogTitle>
<DialogTitle id="form-dialog-title">{isEditMode ? t`Edit category` : t`New category`}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label={t('category.label.category_name')}
label={t`Category Name`}
type="text"
fullWidth
value={dialogName}
onChange={(e) => setDialogName(e.target.value.trim())}
error={isInvalidName}
helperText={isInvalidName ? t`global.error.label.invalid_input` : undefined}
helperText={isInvalidName ? t`Invalid input` : undefined}
/>
<FormControlLabel
control={<Checkbox checked={dialogDefault} onChange={(e) => setDialogDefault(e.target.checked)} />}
label={t('category.label.use_as_default_category')}
label={t`Default category when adding new manga to the library`}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={handleDialogSubmit} color="primary" disabled={!canSubmit}>
{t('global.button.submit')}
{t`Submit`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -9,10 +9,10 @@
import { ComponentProps, useMemo, useState } from 'react';
import Fab from '@mui/material/Fab';
import AddIcon from '@mui/icons-material/Add';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -31,10 +31,10 @@ import { CREATE_NEW_CATEGORY_ID } from '@/features/category/Category.constants.t
import { CreateOrEditCategoryDialog } from '@/features/category/components/CreateOrEditCategoryDialog.tsx';
export function CategorySettings() {
const { t } = useTranslation();
const { t } = useLingui();
const dndSensors = DndKitUtil.useSensorsForDevice();
useAppTitle(t('category.dialog.title.edit_category_other'));
useAppTitle(t`Edit categories`);
const { data, loading, error, refetch } = requestManager.useGetCategories<
GetCategoriesSettingsQuery,
@@ -96,7 +96,7 @@ export function CategorySettings() {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('category.error.label.request_failure')}
message={t`Could not load categories`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('CategorySettings::refetch'))}
/>

View File

@@ -6,8 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { ChapterAction, ChapterListOptions, ChapterSortMode } from '@/features/chapter/Chapter.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isDownloaded: false, isBookmarked: false };
@@ -21,11 +22,11 @@ export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
excludedScanlators: [],
};
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY: Record<ChapterSortMode, TranslationKey> = {
source: 'global.sort.label.by_source',
chapterNumber: 'global.sort.label.by_chapter_number',
uploadedAt: 'global.sort.label.by_upload_date',
fetchedAt: 'global.sort.label.by_fetch_date',
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION: Record<ChapterSortMode, MessageDescriptor> = {
source: msg`By source`,
chapterNumber: msg`By chapter number`,
uploadedAt: msg`By upload date`,
fetchedAt: msg`By date fetched`,
};
export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
@@ -43,65 +44,65 @@ export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
export const CHAPTER_ACTION_TO_TRANSLATION: {
[key in ChapterAction]: {
action: {
single: TranslationKey;
selected: TranslationKey;
single: MessageDescriptor;
selected: MessageDescriptor;
};
confirmation?: TranslationKey;
success: TranslationKey;
error: TranslationKey;
confirmation?: MessageDescriptor;
success: MessageDescriptor;
error: MessageDescriptor;
};
} = {
download: {
action: {
single: 'chapter.action.download.add.label.action',
selected: 'chapter.action.download.add.button.selected',
single: msg`Download`,
selected: msg`Download selected`,
},
confirmation: 'chapter.action.download.add.label.confirmation',
success: 'chapter.action.download.add.label.success',
error: 'chapter.action.download.add.label.error',
confirmation: msg`{count, plural, one {You are about to download one chapter} other {You are about to download # chapters.\nSuwayomi is not a mass downloader and too many downloads can get you banned from sources and/or cause performance issues.}}`,
success: msg`{count, plural, one {Download added} other {# downloads added}}`,
error: msg`{count, plural, one {Could not add the download} other {Could not add downloads}}`,
},
delete: {
action: {
single: 'chapter.action.download.delete.label.action',
selected: 'chapter.action.download.delete.button.selected',
single: msg`Delete`,
selected: msg`Delete selected`,
},
confirmation: 'chapter.action.download.delete.label.confirmation',
success: 'chapter.action.download.delete.label.success',
error: 'chapter.action.download.delete.label.error',
confirmation: msg`{count, plural, one {You are about to delete one download} other {You are about to delete # downloads}}`,
success: msg`{count, plural, one {Chapter deleted} other {# chapters deleted}}`,
error: msg`{count, plural, one {Could not delete the chapter} other {Could not delete chapters}}`,
},
bookmark: {
action: {
single: 'chapter.action.bookmark.add.label.action',
selected: 'chapter.action.bookmark.add.button.selected',
single: msg`Add bookmark`,
selected: msg`Bookmark selected`,
},
success: 'chapter.action.bookmark.add.label.success',
error: 'chapter.action.bookmark.add.label.error',
success: msg`{count, plural, one {Chapter bookmarked} other {# chapters bookmarked}}`,
error: msg`{count, plural, one {Could not bookmark the chapter} other {Could not bookmark chapters}}`,
},
unbookmark: {
action: {
single: 'chapter.action.bookmark.remove.label.action',
selected: 'chapter.action.bookmark.remove.button.selected',
single: msg`Remove bookmark`,
selected: msg`Remove bookmarks from selected`,
},
confirmation: 'chapter.action.bookmark.remove.label.confirmation',
success: 'chapter.action.bookmark.remove.label.success',
error: 'chapter.action.bookmark.remove.label.error',
confirmation: msg`{count, plural, one {You are about to remove one bookmark} other {You are about to remove # bookmarks}}`,
success: msg`{count, plural, one {Chapter bookmark removed} other {# chapter bookmarks removed}}`,
error: msg`{count, plural, one {Could not remove the bookmark} other {Could not remove the bookmarks}}`,
},
mark_as_read: {
action: {
single: 'chapter.action.mark_as_read.add.label.action.current',
selected: 'chapter.action.mark_as_read.add.button.selected',
single: msg`Mark as read`,
selected: msg`Mark selected as read`,
},
confirmation: 'chapter.action.mark_as_read.add.label.confirmation',
success: 'chapter.action.mark_as_read.add.label.success',
error: 'chapter.action.mark_as_read.add.label.error',
confirmation: msg`{count, plural, one {You are about to mark one chapter as read} other {You are about to mark # chapters as read}}`,
success: msg`{count, plural, one {Chapter marked as read} other {# chapters marked as read}}`,
error: msg`{count, plural, one {Could not mark the chapter as read} other {Could not mark chapters as read}}`,
},
mark_as_unread: {
action: {
single: 'chapter.action.mark_as_read.remove.label.action',
selected: 'chapter.action.mark_as_read.remove.button.selected',
single: msg`Mark as unread`,
selected: msg`Mark selected as unread`,
},
confirmation: 'chapter.action.mark_as_read.remove.label.confirmation',
success: 'chapter.action.mark_as_read.remove.label.success',
error: 'chapter.action.mark_as_read.remove.label.error',
confirmation: msg`{count, plural, one {You are about to mark one chapter as unread} other {You are about to mark # chapters as unread}}`,
success: msg`{count, plural, one {Chapter marked as unread} other {# chapters marked as unread}}`,
error: msg`{count, plural, one {Could not mark the chapter as unread} other {Could not mark chapters as unread}}`,
},
};

View File

@@ -7,9 +7,9 @@
*/
import { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
import { useTranslation } from 'react-i18next';
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined';
import DisabledByDefaultRounded from '@mui/icons-material/DisabledByDefaultRounded';
import { useLingui } from '@lingui/react/macro';
import { CheckboxListSetting } from '@/base/components/settings/CheckboxListSetting.tsx';
import { updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
@@ -23,7 +23,7 @@ export const ChapterExcludeSanlatorsFilter = ({
scanlators: string[];
excludedScanlators: string[];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const popupState = usePopupState({ variant: 'dialog', popupId: 'chapter-list-options-scanlator-filter-dialog' });
if (!scanlators.length) {
@@ -34,13 +34,13 @@ export const ChapterExcludeSanlatorsFilter = ({
<>
<CheckboxInput
{...bindTrigger(popupState)}
label={t('global.label.scanlator')}
label={t`Scanlator`}
icon={<PeopleAltOutlinedIcon />}
checkedIcon={<PeopleAltOutlinedIcon color="warning" />}
checked={!!excludedScanlators.length}
/>
<CheckboxListSetting
title={t('chapter.option.exclude_scanlators')}
title={t`Exclude scanlators`}
open={popupState.isOpen}
onClose={(selectedScanlators) => {
if (selectedScanlators) {

View File

@@ -11,7 +11,8 @@ import Stack from '@mui/material/Stack';
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { ComponentProps, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ResumeFab } from '@/features/manga/components/ResumeFAB.tsx';
import {
@@ -82,7 +83,7 @@ const ChapterListFAB = ({
}) => {
if (selectedChapters.length) {
return (
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
<SelectionFAB title={plural(selectedChapters.length, { one: '# chapter', other: '# chapters' })}>
{(handleClose) => (
<ChapterActionMenuItems
selectedChapters={selectedChapters}
@@ -110,7 +111,7 @@ export const ChapterList = ({
manga: Pick<MangaScreenFieldsFragment, 'id' | 'firstUnreadChapter' | 'chapters' | 'unreadCount' | 'downloadCount'>;
isRefreshing: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const { appBarHeight } = useNavBarContext();
const isMobileWidth = MediaQuery.useIsBelowWidth('md');
@@ -126,7 +127,7 @@ export const ChapterList = ({
const options = useChapterListOptions(manga);
const updateOption = updateChapterListOptions(manga, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
const {
data: chaptersData,
@@ -173,7 +174,7 @@ export const ChapterList = ({
return (
<Stack sx={{ justifyContent: 'center', position: 'relative', flexGrow: 1 }}>
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('ChapterList::refetch'))}
/>
@@ -193,13 +194,17 @@ export const ChapterList = ({
>
<Stack>
<Typography variant="h5" component="h3">
{t('chapter.value', { count: visibleChapters.length })}
{plural(visibleChapters.length, {
one: '# chapter',
other: '# chapters',
})}
</Typography>
{!!missingChapterCount && (
<Typography variant="body2" color="warning">
{`${t('chapter.missing', {
count: missingChapterCount,
})}`}
{plural(missingChapterCount, {
one: 'Missing # chapter',
other: 'Missing # chapters',
})}
</Typography>
)}
</Stack>
@@ -225,10 +230,8 @@ export const ChapterList = ({
</Stack>
</ChapterListHeader>
{noChaptersFound && <EmptyViewAbsoluteCentered message={t('chapter.error.label.no_chapter_found')} />}
{noChaptersMatchingFilter && (
<EmptyViewAbsoluteCentered message={t('chapter.error.label.no_matches')} />
)}
{noChaptersFound && <EmptyViewAbsoluteCentered message={t`No chapters found`} />}
{noChaptersMatchingFilter && <EmptyViewAbsoluteCentered message={t`No chapters matching filter`} />}
<StyledVirtuoso
persistKey={`manga-${manga.id}-chapter-list`}

View File

@@ -6,18 +6,19 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import RadioGroup from '@mui/material/RadioGroup';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { RadioInput } from '@/base/components/inputs/RadioInput.tsx';
import { SortRadioInput } from '@/base/components/inputs/SortRadioInput.tsx';
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
import { OptionsTabs } from '@/base/components/modals/OptionsTabs.tsx';
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/features/chapter/Chapter.constants.ts';
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
import { ChapterListOptions } from '@/features/chapter/Chapter.types.ts';
import { updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
import { ChapterExcludeSanlatorsFilter } from '@/features/chapter/components/ChapterExcludeSanlatorsFilter.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
interface IProps {
open: boolean;
@@ -28,10 +29,10 @@ interface IProps {
excludedScanlators: string[];
}
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
filter: 'global.label.filter',
sort: 'global.label.sort',
display: 'global.label.display',
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
filter: msg`Filter`,
sort: msg`Sort`,
display: msg`Display`,
};
export const ChapterOptions: React.FC<IProps> = ({
@@ -42,7 +43,7 @@ export const ChapterOptions: React.FC<IProps> = ({
scanlators,
excludedScanlators,
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<OptionsTabs<'filter' | 'sort' | 'display'>
@@ -56,17 +57,17 @@ export const ChapterOptions: React.FC<IProps> = ({
return (
<>
<ThreeStateCheckboxInput
label={t('global.filter.label.unread')}
label={t`Unread`}
checked={options.unread}
onChange={(c) => updateOption('unread', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.downloaded')}
label={t`Downloaded`}
checked={options.downloaded}
onChange={(c) => updateOption('downloaded', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.bookmarked')}
label={t`Bookmarked`}
checked={options.bookmarked}
onChange={(c) => updateOption('bookmarked', c)}
/>
@@ -79,7 +80,7 @@ export const ChapterOptions: React.FC<IProps> = ({
);
}
if (key === 'sort') {
return Object.entries(CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY).map(([mode, label]) => (
return Object.entries(CHAPTER_SORT_OPTIONS_TO_TRANSLATION).map(([mode, label]) => (
<SortRadioInput
key={mode}
label={t(label)}
@@ -87,10 +88,7 @@ export const ChapterOptions: React.FC<IProps> = ({
sortDescending={options.reverse}
onClick={() =>
mode !== options.sortBy
? updateOption(
'sortBy',
mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY,
)
? updateOption('sortBy', mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION)
: updateOption('reverse', !options.reverse)
}
/>
@@ -102,8 +100,8 @@ export const ChapterOptions: React.FC<IProps> = ({
onChange={() => updateOption('showChapterNumber', !options.showChapterNumber)}
value={options.showChapterNumber}
>
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} />
<RadioInput label={t('chapter.option.display.label.chapter_number')} value />
<RadioInput label={t`Source title`} value={false} />
<RadioInput label={t`Chapter number`} value />
</RadioGroup>
);
}

View File

@@ -9,12 +9,12 @@
import FilterList from '@mui/icons-material/FilterList';
import IconButton from '@mui/material/IconButton';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import Menu from '@mui/material/Menu';
import DownloadIcon from '@mui/icons-material/Download';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ChapterOptions } from '@/features/chapter/components/ChapterOptions.tsx';
import { isFilterActive, updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
@@ -45,7 +45,7 @@ export const ChaptersToolbarMenu = ({
scanlators,
excludeScanlators,
}: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [open, setOpen] = React.useState(false);
const isFiltered = isFilterActive(options);
@@ -55,7 +55,7 @@ export const ChaptersToolbarMenu = ({
return (
<>
<CustomTooltip title={t('chapter.action.mark_as_read.add.label.action.all')} disabled={areAllChaptersRead}>
<CustomTooltip title={t`Mark all as read`} disabled={areAllChaptersRead}>
<IconButton
disabled={areAllChaptersRead}
onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, mangaId)}
@@ -67,7 +67,7 @@ export const ChaptersToolbarMenu = ({
<PopupState variant="popover" popupId="chapterlist-download-button">
{(popupState) => (
<>
<CustomTooltip title={t('global.button.download')} disabled={areAllChaptersRead}>
<CustomTooltip title={t`Download`} disabled={areAllChaptersRead}>
<IconButton
disabled={areAllChaptersDownloaded}
{...bindTrigger(popupState)}
@@ -84,7 +84,7 @@ export const ChaptersToolbarMenu = ({
</>
)}
</PopupState>
<CustomTooltip title={t('chapter.action.filter_and_sort.label')}>
<CustomTooltip title={t`Filter and sort`}>
<IconButton onClick={() => setOpen(true)} color="inherit">
<FilterList color={isFiltered ? 'warning' : undefined} />
</IconButton>

View File

@@ -17,40 +17,43 @@
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { plural } from '@lingui/core/macro';
export const MissingChaptersInfoSeparator = ({ missingChaptersGap }: { missingChaptersGap: number }) => {
const { t } = useTranslation();
return (
<Stack
export const MissingChaptersInfoSeparator = ({
missingChaptersGap: missingChapterCount,
}: {
missingChaptersGap: number;
}) => (
<Stack
sx={{
width: '100%',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
p: 2,
pt: 3.5,
pb: 2.5,
}}
>
<Box
sx={{
width: '100%',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
p: 2,
pt: 3.5,
pb: 2.5,
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
{t('chapter.missing', { count: missingChaptersGap })}
</Typography>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
</Stack>
);
};
/>
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
{plural(missingChapterCount, {
one: 'Missing # chapter',
other: 'Missing # chapters',
})}
</Typography>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
</Stack>
);

View File

@@ -11,11 +11,11 @@ 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 BookmarkRemove from '@mui/icons-material/BookmarkRemove';
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
import DoneAll from '@mui/icons-material/DoneAll';
import { ComponentProps, useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { MenuItem } from '@/base/components/menu/MenuItem.tsx';
@@ -74,7 +74,7 @@ export const ChapterActionMenuItems = ({
onClose,
selectable = true,
}: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
const isSingleMode = !!chapter;
const { isDownloaded, isRead, isBookmarked } = chapter ?? {};
@@ -173,7 +173,7 @@ export const ChapterActionMenuItems = ({
return (
<>
{isSingleMode && selectable && (
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t`Select`} />
)}
{isSingleMode && (
<>
@@ -184,7 +184,7 @@ export const ChapterActionMenuItems = ({
window.open(chapter!.realUrl!, '_blank', 'noopener,noreferrer');
onClose();
}}
title={t('global.button.open_browser')}
title={t`Open in browser`}
/>
<MenuItem
Icon={IconWebView}
@@ -197,7 +197,7 @@ export const ChapterActionMenuItems = ({
);
onClose();
}}
title={t('global.button.open_webview')}
title={t`Open in WebView`}
/>
</>
)}
@@ -255,7 +255,7 @@ export const ChapterActionMenuItems = ({
<MenuItem
onClick={() => performAction('mark_prev_as_read', [])}
Icon={DoneAll}
title={t('chapter.action.mark_as_read.add.label.action.previous')}
title={t`Mark previous as read`}
/>
)}
</>

View File

@@ -6,9 +6,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
import gql from 'graphql-tag';
import { msg } from '@lingui/core/macro';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
@@ -28,26 +29,46 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { TranslationKey } from '@/base/Base.types.ts';
import { i18n } from '@/i18n';
const DOWNLOAD_OPTIONS: {
title: TranslationKey;
title: MessageDescriptor;
getCount: (downloadAheadLimit: number) => number | undefined;
onlyUnread?: boolean;
isDownloadAhead?: boolean;
}[] = [
{ title: 'chapter.action.download.add.label.next', getCount: () => 1 },
{ title: 'chapter.action.download.add.label.next', getCount: () => 5 },
{ title: 'chapter.action.download.add.label.next', getCount: () => 10 },
{ title: 'chapter.action.download.add.label.next', getCount: () => 25 },
{
title: 'chapter.action.download.add.label.ahead',
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 1,
},
{
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 5,
},
{
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 10,
},
{
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
getCount: () => 25,
},
{
title: msg`Download ahead ({count})`,
getCount: (downloadAheadLimit) => downloadAheadLimit,
onlyUnread: true,
isDownloadAhead: true,
},
{ title: 'chapter.action.download.add.label.unread', getCount: () => undefined, onlyUnread: true },
{ title: 'chapter.action.download.add.label.all', getCount: () => undefined, onlyUnread: false },
{
title: msg`Unread`,
getCount: () => undefined,
onlyUnread: true,
},
{
title: msg`All`,
getCount: () => undefined,
onlyUnread: false,
},
];
const handleDownload = async (
@@ -131,8 +152,6 @@ export const ChaptersDownloadActionMenuItems = ({
mangaIds: MangaType['id'][];
closeMenu: () => void;
}) => {
const { t } = useTranslation();
const {
settings: { downloadAheadLimit },
} = useMetadataServerSettings();
@@ -140,8 +159,10 @@ export const ChaptersDownloadActionMenuItems = ({
const handleSelect = (size?: number, onlyUnread: boolean = true, downloadAhead: boolean = false) => {
handleDownload(mangaIds, onlyUnread, size, downloadAhead).catch((e) =>
makeToast(
t(CHAPTER_ACTION_TO_TRANSLATION.download.error, {
count: size,
/* lingui-extract-ignore */
i18n.t({
...CHAPTER_ACTION_TO_TRANSLATION.download.error,
values: { count: size },
}),
'error',
getErrorMessage(e),
@@ -155,10 +176,16 @@ export const ChaptersDownloadActionMenuItems = ({
<>
{DOWNLOAD_OPTIONS.map(({ title, getCount, onlyUnread, isDownloadAhead }) => (
<MenuItem
key={t(title, { count: getCount(downloadAheadLimit) })}
key={
/* lingui-extract-ignore */
i18n.t({ ...title, values: { count: getCount(downloadAheadLimit) } })
}
onClick={() => handleSelect(getCount(downloadAheadLimit), onlyUnread, isDownloadAhead)}
>
{t(title, { count: getCount(downloadAheadLimit) })}
{
/* lingui-extract-ignore */
i18n.t({ ...title, values: { count: getCount(downloadAheadLimit) } })
}
</MenuItem>
))}
</>

View File

@@ -7,8 +7,8 @@
*/
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import DownloadIcon from '@mui/icons-material/Download';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -24,15 +24,13 @@ export const ChapterDownloadButton = ({
chapterId: ChapterIdInfo['id'];
isDownloaded: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const download = Chapters.useDownloadStatusFromCache(chapterId);
const downloadChapter = () => {
requestManager
.addChapterToDownloadQueue(chapterId)
.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)));
};
if (download == null && isDownloaded) {
@@ -40,7 +38,7 @@ export const ChapterDownloadButton = ({
}
return (
<CustomTooltip title={t('chapter.action.download.add.label.action')}>
<CustomTooltip title={t`Download`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -8,7 +8,7 @@
import Refresh from '@mui/icons-material/Refresh';
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
@@ -19,14 +19,14 @@ import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => {
const { t } = useTranslation();
const { t } = useLingui();
const download = Chapters.useDownloadStatusFromCache(chapterId);
const handleRetry = async () => {
try {
await requestManager.addChapterToDownloadQueue(chapterId).response;
} catch (e) {
makeToast(t('download.queue.error.label.failed_to_retry'), 'error', getErrorMessage(e));
makeToast(t`Could not retry failed download.`, 'error', getErrorMessage(e));
}
};
@@ -35,7 +35,7 @@ export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterId
}
return (
<CustomTooltip title={t('global.button.retry')}>
<CustomTooltip title={t`Retry`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -16,9 +16,9 @@ import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import React, { memo, MouseEvent, TouchEvent, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import { useLongPress } from 'use-long-press';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { getDateString } from '@/base/utils/DateHelper.ts';
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
@@ -61,7 +61,7 @@ interface IProps {
}
export const ChapterCard = memo((props: IProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const preventMobileContextMenu = MediaQuery.usePreventMobileContextMenu();
@@ -134,13 +134,11 @@ export const ChapterCard = memo((props: IProps) => {
>
<ListCardContent>
<ChapterCardMetadata
title={
showChapterNumber
? `${t('chapter.title_one')} ${chapter.chapterNumber}`
: chapter.name
}
title={showChapterNumber ? `${t`Chapter`} ${chapter.chapterNumber}` : chapter.name}
secondaryText={chapter.scanlator}
ternaryText={`${getDateString(Number(chapter.uploadDate ?? 0), true)}${isDownloaded ? `${t('chapter.status.label.downloaded')}` : ''}`}
ternaryText={`${getDateString(Number(chapter.uploadDate ?? 0), true)}${
isDownloaded ? `${t`Downloaded`}` : ''
}`}
infoIcons={
chapter.isBookmarked && (
<BookmarkIcon
@@ -180,7 +178,7 @@ export const ChapterCard = memo((props: IProps) => {
<Stack sx={{ minHeight: '48px' }}>
{selected === null ? (
<CustomTooltip title={t('global.button.options')}>
<CustomTooltip title={t`Options`}>
<IconButton
ref={menuButtonRef}
{...MUIUtil.preventRippleProp(bindTrigger(popupState), {
@@ -198,9 +196,7 @@ export const ChapterCard = memo((props: IProps) => {
</IconButton>
</CustomTooltip>
) : (
<CustomTooltip
title={t(selected ? 'global.button.deselect' : 'global.button.select')}
>
<CustomTooltip title={selected ? t`Deselect` : t`Select`}>
<Checkbox checked={selected} />
</CustomTooltip>
)}

View File

@@ -6,8 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t as translate } from 'i18next';
import { DocumentNode, MaybeMasked, Unmasked, useFragment } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { makeToast } from '@/base/utils/Toast.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
@@ -18,8 +18,8 @@ import {
DownloadTypeFieldsFragment,
} from '@/lib/graphql/generated/graphql.ts';
import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/chapter/ChapterFragments.ts';
import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
import { ReaderOpenChapterLocationState, ReaderResumeMode } from '@/features/reader/Reader.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
@@ -43,6 +43,7 @@ import {
import { assertIsDefined } from '@/base/Asserts.ts';
import { DirectionOffset } from '@/base/Base.types.ts';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
import { i18n } from '@/i18n';
export class Chapters {
static getIds(chapters: { id: number }[]): number[] {
@@ -276,11 +277,12 @@ export class Chapters {
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`,
},
},
});
@@ -290,10 +292,15 @@ export class Chapters {
}
await fnToExecute();
makeToast(translate(CHAPTER_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success');
makeToast(
/* lingui-extract-ignore */
i18n.t({ ...CHAPTER_ACTION_TO_TRANSLATION[action].success, values: { count: itemCount } }),
'success',
);
} catch (e) {
makeToast(
translate(CHAPTER_ACTION_TO_TRANSLATION[action].error, { count: itemCount }),
/* lingui-extract-ignore */
i18n.t({ ...CHAPTER_ACTION_TO_TRANSLATION[action].error, values: { count: itemCount } }),
'error',
getErrorMessage(e),
);

View File

@@ -10,7 +10,7 @@ import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import {
updateMetadataServerSettings,
useMetadataServerSettings,
@@ -28,9 +28,9 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export const DeviceSetting = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.device.title.device'));
useAppTitle(t`Device`);
const {
metadata,
@@ -55,7 +55,7 @@ export const DeviceSetting = () => {
}
updateMetadataServerSettings(setting, value).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -66,7 +66,7 @@ export const DeviceSetting = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('DeviceSetting::refetch'))}
/>
@@ -76,32 +76,30 @@ export const DeviceSetting = () => {
return (
<List sx={{ pt: 0 }}>
<MutableListSetting
settingName={t('settings.device.devices.label.title')}
description={t('settings.device.devices.label.description')}
settingName={t`Devices`}
description={t`Manage your existing devices.\nUI specific settings that are stored on the server are per device.\nThis makes it possible to have e.g. different settings on a desktop and a smartphone`}
handleChange={(deviceList) => {
updateMetadataSetting('devices', [
...new Set(
[DEFAULT_DEVICE, ...deviceList].filter((device) => device !== t('global.label.default')),
),
...new Set([DEFAULT_DEVICE, ...deviceList].filter((device) => device !== t`Default`)),
]);
}}
valueInfos={devices.map((device) => [
device === DEFAULT_DEVICE ? t('global.label.default') : device,
device === DEFAULT_DEVICE ? t`Default` : device,
{ mutable: false, deletable: device !== DEFAULT_DEVICE },
])}
addItemButtonTitle={t('global.button.create')}
addItemButtonTitle={t`Create`}
validateItem={(device) => device.length <= 16 && !!device.match(/^[a-zA-Z0-9\-_]+$/g)}
placeholder={t('settings.device.label.placeholder')}
placeholder={t`Smartphone_Name-1 | length: 16, chars: letters, numbers, -, _`}
/>
<ListItem>
<ListItemText
primary={t('settings.device.active_device.label.title')}
secondary={t('settings.device.active_device.label.description')}
primary={t`Active device`}
secondary={t`Select a device to use its server stored UI settings`}
/>
<Select value={activeDevice} onChange={({ target: { value: device } }) => setActiveDevice(device)}>
{devices.map((device) => (
<MenuItem key={device} value={device}>
{device === DEFAULT_DEVICE ? t('global.label.default') : device}
{device === DEFAULT_DEVICE ? t`Default` : device}
</MenuItem>
))}
</Select>

View File

@@ -6,7 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import {
SelectSetting,
SelectSettingValue,
@@ -14,33 +15,30 @@ import {
} from '@/base/components/settings/SelectSetting.tsx';
const CHAPTERS_TO_DELETE = [0, 1, 2, 3, 4, 5] as const;
const CHAPTERS_TO_DELETE_TO_TRANSLATION_KEY: {
const CHAPTERS_TO_DELETE_TO_TRANSLATION: {
[flavor in (typeof CHAPTERS_TO_DELETE)[number]]: SelectSettingValueDisplayInfo;
} = {
0: {
text: 'global.label.disabled',
text: msg`Disabled`,
},
1: {
text: 'download.settings.delete_chapters.while_reading.option.label.first',
text: msg`Last read chapter`,
},
2: {
text: 'download.settings.delete_chapters.while_reading.option.label.second',
text: msg`Second to last read chapter`,
},
3: {
text: 'download.settings.delete_chapters.while_reading.option.label.third',
text: msg`Third to last read chapter`,
},
4: {
text: 'download.settings.delete_chapters.while_reading.option.label.fourth',
text: msg`Fourth to last read chapter`,
},
5: {
text: 'download.settings.delete_chapters.while_reading.option.label.fifth',
text: msg`Fifth to last read chapter`,
},
};
const CHAPTERS_TO_DELETE_SELECT_VALUES: SelectSettingValue<(typeof CHAPTERS_TO_DELETE)[number]>[] =
CHAPTERS_TO_DELETE.map((chapterToDelete) => [
chapterToDelete,
CHAPTERS_TO_DELETE_TO_TRANSLATION_KEY[chapterToDelete],
]);
CHAPTERS_TO_DELETE.map((chapterToDelete) => [chapterToDelete, CHAPTERS_TO_DELETE_TO_TRANSLATION[chapterToDelete]]);
const getNormalizedChapterToDelete = (chapterToDelete: number | boolean) => {
const isMigrationVersion0 = typeof chapterToDelete === 'boolean';
@@ -58,13 +56,13 @@ export const DeleteChaptersWhileReadingSetting = ({
chapterToDelete: number;
handleChange: (chapterToDelete: number) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const normalizedChapterToDelete = getNormalizedChapterToDelete(chapterToDelete);
return (
<SelectSetting
settingName={t('download.settings.delete_chapters.while_reading.label.title')}
settingName={t`Delete finished chapters while reading`}
value={normalizedChapterToDelete}
values={CHAPTERS_TO_DELETE_SELECT_VALUES}
handleChange={handleChange}

View File

@@ -6,11 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/base/hooks/usePersistedValue.tsx';
import { updateMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
@@ -25,7 +26,7 @@ export const DownloadAheadSetting = ({
}: {
downloadAheadLimit: MetadataServerSettings['downloadAheadLimit'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const shouldDownloadAhead = !!downloadAheadLimit;
const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue(
@@ -38,7 +39,7 @@ export const DownloadAheadSetting = ({
const updateSetting = (value: MetadataDownloadSettings['downloadAheadLimit']) => {
persistDownloadAheadLimit(value === 0 ? currentDownloadAheadLimit : value);
updateMetadataServerSettings('downloadAheadLimit', value).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
};
@@ -50,14 +51,14 @@ export const DownloadAheadSetting = ({
return (
<List>
<ListItem>
<ListItemText primary={t('download.settings.download_ahead.label.while_reading')} />
<ListItemText primary={t`Auto download while reading`} />
<Switch edge="end" checked={shouldDownloadAhead} onChange={(e) => setDoAutoUpdates(e.target.checked)} />
</ListItem>
<NumberSetting
settingTitle={t('download.settings.download_ahead.label.unread_chapters_to_download')}
settingValue={t('download.settings.download_ahead.label.value', {
chapters: currentDownloadAheadLimit,
count: currentDownloadAheadLimit,
settingTitle={t`Number of unread chapters to download`}
settingValue={plural(currentDownloadAheadLimit, {
one: '# Chapter',
other: '# Chapters',
})}
value={currentDownloadAheadLimit}
minValue={DOWNLOAD_AHEAD.min}
@@ -65,9 +66,9 @@ export const DownloadAheadSetting = ({
defaultValue={DOWNLOAD_AHEAD.default}
stepSize={DOWNLOAD_AHEAD.step}
showSlider
dialogDescription={t('download.settings.download_ahead.label.description')}
dialogDisclaimer={t('download.settings.download_ahead.label.disclaimer')}
valueUnit={t('chapter.title_one')}
dialogDescription={t`How many chapters should get downloaded while reading.`}
dialogDisclaimer={t`Only works if the current chapter plus the next chapter are already downloaded.`}
valueUnit={t`Chapter`}
handleUpdate={updateSetting}
disabled={!shouldDownloadAhead}
/>

View File

@@ -14,7 +14,7 @@ import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import { memo, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ChapterDownloadRetryButton } from '@/features/chapter/components/buttons/ChapterDownloadRetryButton.tsx';
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
@@ -32,7 +32,7 @@ import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
export const DownloadQueueChapterCard = memo(
({ item, status }: { item: ChapterDownloadStatus; status: DownloaderState }) => {
const { t } = useTranslation();
const { t } = useLingui();
const preventMobileContextMenu = MediaQuery.usePreventMobileContextMenu();
const handleDelete = useCallback(
@@ -53,7 +53,7 @@ export const DownloadQueueChapterCard = memo(
requestManager.deleteDownloadedChapter(chapter.id).response,
]);
} catch (e) {
makeToast(t('download.queue.error.label.failed_to_remove'), 'error', getErrorMessage(e));
makeToast(t`Could not remove the download from the queue.`, 'error', getErrorMessage(e));
}
if (!isRunning) {
@@ -83,7 +83,7 @@ export const DownloadQueueChapterCard = memo(
<ChapterCardMetadata title={item.manga.title} secondaryText={item.chapter.name} />
<DownloadStateIndicator chapterId={item.chapter.id} />
<ChapterDownloadRetryButton chapterId={item.chapter.id} />
<CustomTooltip title={t('chapter.action.download.delete.label.action')}>
<CustomTooltip title={t`Delete`}>
<IconButton
{...MUIUtil.preventRippleProp()}
onClick={(e) => {

View File

@@ -11,11 +11,11 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useWindowEvent } from '@mantine/hooks';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
@@ -34,9 +34,9 @@ import { ChapterDownloadStatus } from '@/features/chapter/Chapter.types.ts';
import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx';
export const DownloadQueue: React.FC = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('download.title.queue'));
useAppTitle(t`Download queue`);
const [reorderDownload, { reset: revertReorder }] = requestManager.useReorderChapterInDownloadQueue();
@@ -60,7 +60,7 @@ export const DownloadQueue: React.FC = () => {
try {
await requestManager.clearDownloads().response;
} catch (e) {
makeToast(t('download.queue.error.label.failed_delete_all'), 'error', getErrorMessage(e));
makeToast(t`Could not remove all downloads from the queue`, 'error', getErrorMessage(e));
}
};
@@ -99,16 +99,13 @@ export const DownloadQueue: React.FC = () => {
useAppAction(
<>
<CustomTooltip title={t('download.queue.label.delete_all')}>
<CustomTooltip title={t`Delete all`}>
<IconButton onClick={clearQueue} color="inherit">
<DeleteSweepIcon />
</IconButton>
</CustomTooltip>
<CustomTooltip
title={t(status === DownloaderState.Started ? 'global.button.stop' : 'global.button.start')}
disabled={isQueueEmpty}
>
<CustomTooltip title={status === DownloaderState.Started ? t`Stop` : t`Start`} disabled={isQueueEmpty}>
<IconButton onClick={toggleQueueStatus} disabled={isQueueEmpty} color="inherit">
{status === DownloaderState.Stopped ? <PlayArrowIcon /> : <PauseIcon />}
</IconButton>
@@ -135,7 +132,7 @@ export const DownloadQueue: React.FC = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('DownloadQueue::refetch'))}
/>
@@ -143,7 +140,7 @@ export const DownloadQueue: React.FC = () => {
}
if (isQueueEmpty) {
return <EmptyViewAbsoluteCentered message={t('download.queue.label.no_downloads')} />;
return <EmptyViewAbsoluteCentered message={t`No downloads`} />;
}
return (

View File

@@ -6,12 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import ListSubheader from '@mui/material/ListSubheader';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DownloadAheadSetting } from '@/features/downloads/components/DownloadAheadSetting.tsx';
@@ -47,9 +48,9 @@ type DownloadSettingsType = Pick<
>;
export const DownloadSettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('download.title.download'));
useAppTitle(t`Downloads`);
const categories = requestManager.useGetCategories<GetCategoriesSettingsQuery, GetCategoriesSettingsQueryVariables>(
GET_CATEGORIES_SETTINGS,
@@ -71,7 +72,7 @@ export const DownloadSettings = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (serverSettings.error) {
@@ -101,28 +102,28 @@ export const DownloadSettings = () => {
value: DownloadSettingsType[Setting],
): Promise<any> => {
const mutation = mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
mutation.catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
mutation.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
return mutation;
};
const updateMetadataSetting = createUpdateMetadataServerSettings<keyof MetadataDownloadSettings>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
return (
<List sx={{ pt: 0 }}>
<TextSetting
settingName={t('download.settings.download_path.label.title')}
dialogDescription={t('download.settings.download_path.label.description')}
settingName={t`Download location`}
dialogDescription={t`The path to the directory on the server where downloaded files should get saved in`}
value={downloadSettings?.downloadsPath}
settingDescription={
downloadSettings?.downloadsPath.length ? downloadSettings.downloadsPath : t('global.label.default')
downloadSettings?.downloadsPath.length ? downloadSettings.downloadsPath : t`Default`
}
handleChange={(path) => updateSetting('downloadsPath', path)}
/>
<ListItem>
<ListItemText primary={t('download.settings.file_type.label.cbz')} />
<ListItemText primary={t`Save as CBZ archive`} />
<Switch
edge="end"
checked={!!downloadSettings?.downloadAsCbz}
@@ -130,17 +131,17 @@ export const DownloadSettings = () => {
/>
</ListItem>
<ListItemLink to={AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads.path}>
<ListItemText primary={t('download.settings.conversion.title')} />
<ListItemText primary={t`Image download processing`} />
</ListItemLink>
<List
subheader={
<ListSubheader component="div" id="download-settings-auto-delete-downloads">
{t('download.settings.delete_chapters.title')}
{t`Delete chapters`}
</ListSubheader>
}
>
<ListItem>
<ListItemText primary={t('download.settings.delete_chapters.label.manually_marked_as_read')} />
<ListItemText primary={t`Delete chapter after manually marking it as read`} />
<Switch
edge="end"
checked={metadataSettings.deleteChaptersManuallyMarkedRead}
@@ -154,7 +155,7 @@ export const DownloadSettings = () => {
}
/>
<ListItem>
<ListItemText primary={t('download.settings.delete_chapters.label.allow_deletion_of_bookmarked')} />
<ListItemText primary={t`Allow deleting bookmarked chapters`} />
<Switch
edge="end"
checked={metadataSettings.deleteChaptersWithBookmark}
@@ -165,12 +166,12 @@ export const DownloadSettings = () => {
<List
subheader={
<ListSubheader component="div" id="download-settings-auto-download">
{t('download.settings.auto_download.title')}
{t`Auto-download`}
</ListSubheader>
}
>
<ListItem>
<ListItemText primary={t('download.settings.auto_download.label.new_chapters')} />
<ListItemText primary={t`Download new chapters`} />
<Switch
edge="end"
checked={!!downloadSettings?.autoDownloadNewChapters}
@@ -179,28 +180,28 @@ export const DownloadSettings = () => {
</ListItem>
<NumberSetting
disabled={!downloadSettings?.autoDownloadNewChapters}
settingTitle={t('download.settings.auto_download.download_limit.label.title')}
dialogDescription={t('download.settings.auto_download.download_limit.label.description')}
settingTitle={t`Chapter download limit`}
dialogDescription={t`Limit the amount of new chapters that are going to get downloaded.`}
value={downloadSettings?.autoDownloadNewChaptersLimit ?? 0}
settingValue={
!downloadSettings.autoDownloadNewChaptersLimit
? t('global.label.none')
: t('download.settings.download_ahead.label.value', {
chapters: downloadSettings.autoDownloadNewChaptersLimit,
count: downloadSettings.autoDownloadNewChaptersLimit,
? t`None`
: plural(downloadSettings.autoDownloadNewChaptersLimit, {
one: '# Chapter',
other: '# Chapters',
})
}
defaultValue={0}
minValue={0}
maxValue={20}
showSlider
valueUnit={t('chapter.title_one')}
valueUnit={t`Chapter`}
handleUpdate={(autoDownloadNewChaptersLimit) =>
updateSetting('autoDownloadNewChaptersLimit', autoDownloadNewChaptersLimit)
}
/>
<ListItem>
<ListItemText primary={t('download.settings.auto_download.label.ignore_with_unread_chapters')} />
<ListItemText primary={t`Ignore automatic chapter downloads for entries with unread chapters`} />
<Switch
edge="end"
checked={!!downloadSettings?.excludeEntryWithUnreadChapters}
@@ -209,7 +210,7 @@ export const DownloadSettings = () => {
/>
</ListItem>
<ListItem>
<ListItemText primary={t('download.settings.auto_download.label.ignore_re_uploads')} />
<ListItemText primary={t`Ignore re-uploaded chapters`} />
<Switch
edge="end"
checked={!!downloadSettings?.autoDownloadIgnoreReUploads}
@@ -220,13 +221,13 @@ export const DownloadSettings = () => {
<CategoriesInclusionSetting
categories={categories.data!.categories.nodes}
includeField="includeInDownload"
dialogText={t('download.settings.auto_download.categories.label.include_in_download')}
dialogText={t`Entries in excluded categories will not be downloaded even if they are also in included categories`}
/>
</List>
<List
subheader={
<ListSubheader component="div" id="download-settings-download-ahead">
{t('download.settings.download_ahead.title')}
{t`Download ahead`}
</ListSubheader>
}
>

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 {
ExtensionAction,
ExtensionGroupState,
@@ -14,7 +16,6 @@ import {
InstalledStates,
} from '@/features/extension/Extensions.types.ts';
import { DefaultLanguage } from '@/base/utils/Languages.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const EXTENSION_ACTION_TO_STATE_MAP: { [action in ExtensionAction]: ExtensionState } = {
[ExtensionAction.UPDATE]: ExtensionState.UPDATING,
@@ -28,31 +29,33 @@ export const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]:
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
} as const;
export 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',
export const INSTALLED_STATE_TO_TRANSLATION_MAP: { [installedState in InstalledStates]: MessageDescriptor } = {
[InstalledState.UNINSTALL]: msg`Uninstall`,
[InstalledState.INSTALL]: msg`Install`,
[InstalledState.UPDATE]: msg`Update`,
[InstalledState.OBSOLETE]: msg`Obsolete`,
[InstalledState.UPDATING]: msg`Updating`,
[InstalledState.UNINSTALLING]: msg`Uninstalling`,
[InstalledState.INSTALLING]: msg`Installing`,
} as const;
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP: {
[action in ExtensionAction]: TranslationKey;
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP: {
[action in ExtensionAction]: MessageDescriptor;
} = {
[ExtensionAction.UPDATE]: 'extension.label.update_failed',
[ExtensionAction.INSTALL]: 'extension.label.installation_failed',
[ExtensionAction.UNINSTALL]: 'extension.label.uninstallation_failed',
[ExtensionAction.UPDATE]: msg`{count, plural, one {Could not update the extension} other {Could not update the extensions}}`,
[ExtensionAction.INSTALL]: msg`{count, plural, one {Could not install the extension} other {Could not install the extensions}}`,
[ExtensionAction.UNINSTALL]: msg`{count, plural, one {Could not uninstall the extension} other {Could not uninstall the extensions}}`,
};
export const extensionLanguageToTranslationKey: { [state in ExtensionGroupState | DefaultLanguage]: TranslationKey } = {
[ExtensionGroupState.INSTALLED]: 'extension.state.label.installed',
[ExtensionGroupState.UPDATE_PENDING]: 'extension.state.label.update_pending',
[ExtensionGroupState.OBSOLETE]: 'extension.state.label.obsolete',
[DefaultLanguage.ALL]: 'extension.language.all',
[DefaultLanguage.OTHER]: 'extension.language.other',
[DefaultLanguage.LOCAL_SOURCE]: 'extension.language.other',
[DefaultLanguage.PINNED]: 'global.label.pinned',
[DefaultLanguage.LAST_USED_SOURCE]: 'global.label.last_used',
export const extensionLanguageToTranslation: {
[state in ExtensionGroupState | DefaultLanguage]: MessageDescriptor;
} = {
[ExtensionGroupState.INSTALLED]: msg`Installed`,
[ExtensionGroupState.UPDATE_PENDING]: msg`Update pending`,
[ExtensionGroupState.OBSOLETE]: msg`Obsolete`,
[DefaultLanguage.ALL]: msg`All`,
[DefaultLanguage.OTHER]: msg`Other`,
[DefaultLanguage.LOCAL_SOURCE]: msg`Other`,
[DefaultLanguage.PINNED]: msg`Pinned`,
[DefaultLanguage.LAST_USED_SOURCE]: msg`Last used`,
};

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t } from 'i18next';
import {
ExtensionAction,
ExtensionGroupState,
@@ -23,14 +22,15 @@ import {
toComparableLanguages,
} from '@/base/utils/Languages.ts';
import {
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP,
extensionLanguageToTranslationKey,
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP,
extensionLanguageToTranslation,
} from '@/features/extension/Extensions.constants.ts';
import { enhancedCleanup } from '@/base/utils/Strings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts';
import { i18n } from '@/i18n';
export const getInstalledState = (
isInstalled: boolean,
@@ -65,7 +65,7 @@ export const isExtensionStateOrLanguage = (languageCode: string): boolean =>
export const translateExtensionLanguage = (languageCode: string): string =>
isExtensionStateOrLanguage(languageCode)
? t(extensionLanguageToTranslationKey[languageCode as ExtensionGroupState | DefaultLanguage])
? i18n._(extensionLanguageToTranslation[languageCode as ExtensionGroupState | DefaultLanguage])
: languageCodeToName(languageCode);
export function groupExtensionsByLanguage(extensions: TExtension[]): GroupedExtensionsResult {
@@ -165,7 +165,8 @@ export const updateExtension = async (
}
} catch (e) {
makeToast(
t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[action], { count: 1 }),
/* lingui-extract-ignore */
i18n.t({ ...EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP[action], values: { count: 1 } }),
'error',
getErrorMessage(e),
);

View File

@@ -8,8 +8,8 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import { INSTALLED_STATE_TO_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts';
import { useLingui } from '@lingui/react/macro';
import { INSTALLED_STATE_TO_TRANSLATION_MAP } from '@/features/extension/Extensions.constants.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
import { useBackButton } from '@/base/hooks/useBackButton.ts';
@@ -17,7 +17,7 @@ import { ExtensionAction, InstalledState, TExtension } from '@/features/extensio
export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TExtension) => {
const handleBack = useBackButton();
const { t } = useTranslation();
const { t } = useLingui();
const installedState = getInstalledState(isInstalled, isObsolete, hasUpdate);
@@ -44,7 +44,7 @@ export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TE
}
}}
>
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
{t(INSTALLED_STATE_TO_TRANSLATION_MAP[installedState])}
</Button>
</Box>
);

View File

@@ -7,25 +7,19 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { TExtension } from '@/features/extension/Extensions.types.ts';
import { ExtensionMetadata } from '@/features/extension/info/components/ExtensionMetadata.tsx';
import { languageCodeToName } from '@/base/utils/Languages.ts';
export const Meta = ({ versionName, lang, isNsfw }: TExtension) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<ExtensionMetadata title={t('global.label.version')} value={versionName} />
<ExtensionMetadata title={t('global.language.label.language')} value={languageCodeToName(lang)} />
{isNsfw && (
<ExtensionMetadata
title={t('extension.label.age_rating')}
value="18+"
valueProps={{ color: 'error' }}
/>
)}
<ExtensionMetadata title={t`Version`} value={versionName} />
<ExtensionMetadata title={t`Language`} value={languageCodeToName(lang)} />
{isNsfw && <ExtensionMetadata title={t`Age rating`} value="18+" valueProps={{ color: 'error' }} />}
</Stack>
);
};

View File

@@ -7,13 +7,13 @@
*/
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import Switch from '@mui/material/Switch';
import CardActionArea from '@mui/material/CardActionArea';
import { useLingui } from '@lingui/react/macro';
import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
@@ -29,11 +29,11 @@ import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/feat
export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo) => {
const { id, isConfigurable } = source;
const { t } = useTranslation();
const { t } = useLingui();
const { isEnabled } = useGetSourceMetadata(source);
const updateSetting = createUpdateSourceMetadata(source, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
return (
@@ -45,7 +45,7 @@ export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceCon
{translateExtensionLanguage(Sources.getLanguage(source))}
</Typography>
{isConfigurable && (
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton
component={Link}
to={AppRoutes.sources.childRoutes.configure.path(id)}

View File

@@ -8,9 +8,9 @@
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
@@ -26,10 +26,10 @@ import { ActionButton } from '@/features/extension/info/components/ActionButton.
import { SourceCard } from '@/features/extension/info/components/SourceCard.tsx';
export const ExtensionInfo = () => {
const { t } = useTranslation();
const { t } = useLingui();
const { pkgName } = useParams<{ pkgName: string }>();
useAppTitle(t('source.extension_info.title'));
useAppTitle(t`Extension info`);
const extensionResponse = requestManager.useGetExtension(pkgName);
const sourcesResponse = requestManager.useGetSourceList();
@@ -55,7 +55,7 @@ export const ExtensionInfo = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(extensionResponse.error)}
retry={() => {
if (extensionResponse.error) {

View File

@@ -12,7 +12,6 @@ import Typography from '@mui/material/Typography';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { StringParam, useQueryParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
@@ -23,6 +22,7 @@ import { useElementSize } from '@mantine/hooks';
import IconButton from '@mui/material/IconButton';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { d } from 'koration';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
import { useDebounce } from '@/base/hooks/useDebounce.ts';
@@ -128,7 +128,7 @@ const SourceSearchPreview = React.memo(
emptyQuery: boolean;
} & Pick<MangaCardProps, 'mode'> &
Pick<MetadataBrowseSettings, 'shouldShowOnlySourcesWithResults'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const { id, name, lang } = source;
@@ -163,9 +163,9 @@ const SourceSearchPreview = React.memo(
let errorMessage: string | undefined;
if (error) {
errorMessage = t('search.error.label.source_search_failed');
errorMessage = t`Could not search source`;
} else if (noMangasFound) {
errorMessage = t('manga.error.label.no_mangas_found');
errorMessage = t`No manga found`;
}
if ((!isLoading && !searchString) || emptyQuery) {
@@ -188,7 +188,7 @@ const SourceSearchPreview = React.memo(
<Typography variant="h5">{name}</Typography>
<Typography variant="caption">{translateExtensionLanguage(lang)}</Typography>
</Box>
<CustomTooltip title={t('global.button.show_more')}>
<CustomTooltip title={t`Show more`}>
<IconButton {...MUIUtil.preventRippleProp()}>
<ArrowForwardIcon />
</IconButton>
@@ -232,7 +232,7 @@ const SourceSearchPreview = React.memo(
);
export const SearchAll: React.FC = () => {
const { t } = useTranslation();
const { t } = useLingui();
const navigate = useNavigate();
const { pathname, state } = useLocation<{ mangaTitle?: string; shouldShowOnlyPinnedSources?: boolean }>();
const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize();
@@ -288,11 +288,11 @@ export const SearchAll: React.FC = () => {
);
const updateMetadataSettings = createUpdateMetadataServerSettings<'shouldShowOnlySourcesWithResults'>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
useAppTitleAndAction(
t(isMigrateMode ? 'migrate.search.title' : 'search.title.global_search', { title: state?.mangaTitle }),
isMigrateMode ? t`Migrate "${state?.mangaTitle}"` : t`Global Search`,
<>
<AppbarSearch isClosable={false} />
<SourceLanguageSelect
@@ -312,7 +312,7 @@ export const SearchAll: React.FC = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('SearchAll::refetch'))}
/>
@@ -351,7 +351,7 @@ export const SearchAll: React.FC = () => {
)
}
>
{t('global.label.pinned')}
{t`Pinned`}
</Button>
<Button
startIcon={<DoneAllIcon />}
@@ -369,7 +369,7 @@ export const SearchAll: React.FC = () => {
)
}
>
{t('extension.language.all')}
{t`All`}
</Button>
</Stack>
<Button
@@ -379,7 +379,7 @@ export const SearchAll: React.FC = () => {
updateMetadataSettings('shouldShowOnlySourcesWithResults', !shouldShowOnlySourcesWithResults)
}
>
{t('search.filter.has_results')}
{t`Has results`}
</Button>
</Stack>
<Box sx={{ pt: `${filterHeaderHeight}px` }}>

View File

@@ -8,7 +8,7 @@
import Typography from '@mui/material/Typography';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
@@ -23,9 +23,9 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export const History: React.FC = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('history.title'));
useAppTitle(t`History`);
const {
data: chapterHistoryData,
@@ -66,7 +66,7 @@ export const History: React.FC = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('History::refetch'))}
/>
@@ -74,7 +74,7 @@ export const History: React.FC = () => {
}
if (!isLoading && readEntries.length === 0) {
return <EmptyViewAbsoluteCentered message={t('history.error.label.no_history_available')} />;
return <EmptyViewAbsoluteCentered message={t`You have not read any series yet.`} />;
}
return (

View File

@@ -6,11 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { useLingui } from '@lingui/react/macro';
import {
createUpdateMetadataServerSettings,
useMetadataServerSettings,
@@ -24,16 +24,16 @@ import { MetadataHistorySettings } from '@/features/history/History.types.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export const HistorySettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('history.title'));
useAppTitle(t`History`);
const {
settings: { hideHistory },
request: { loading, error, refetch },
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<keyof MetadataHistorySettings>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
if (loading) {
@@ -43,7 +43,7 @@ export const HistorySettings = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('HistorySettings::refetch'))}
/>
@@ -53,7 +53,7 @@ export const HistorySettings = () => {
return (
<List sx={{ pt: 0 }}>
<ListItem>
<ListItemText primary={t('history.settings.hide')} />
<ListItemText primary={t`Hide history`} />
<Switch
edge="end"
checked={hideHistory}

View File

@@ -7,7 +7,7 @@
*/
import React, { useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IMangaGridProps, MangaGrid } from '@/features/manga/components/MangaGrid.tsx';
import { GridLayout } from '@/base/Base.types.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
@@ -27,7 +27,7 @@ export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
messageExtra,
...gridProps
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const {
settings: { gridLayout },
@@ -46,7 +46,7 @@ export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
{...gridProps}
hasNextPage={false}
loadMore={loadMoreNoop}
message={showFilteredOutMessage ? t('library.error.label.no_matches') : message}
message={showFilteredOutMessage ? t`No manga matches this filter` : message}
messageExtra={showFilteredOutMessage ? undefined : messageExtra}
gridLayout={gridLayout}
/>

View File

@@ -6,9 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import FormLabel from '@mui/material/FormLabel';
import RadioGroup from '@mui/material/RadioGroup';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { RadioInput } from '@/base/components/inputs/RadioInput.tsx';
import { SortRadioInput } from '@/base/components/inputs/SortRadioInput.tsx';
@@ -28,23 +30,23 @@ import {
import { LibrarySortMode } from '@/features/library/Library.types.ts';
import { CategoryMetadataInfo } from '@/features/category/Category.types.ts';
import { MANGA_STATUS_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
import { GridLayout, TranslationKey } from '@/base/Base.types.ts';
import { GridLayout } from '@/base/Base.types';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
filter: 'global.label.filter',
sort: 'global.label.sort',
display: 'global.label.display',
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
filter: msg`Filter`,
sort: msg`Sort`,
display: msg`Display`,
};
const SORT_OPTIONS: [LibrarySortMode, TranslationKey][] = [
['unreadChapters', 'library.option.sort.label.by_unread_chapters'],
['totalChapters', 'library.option.sort.label.by_total_chapters'],
['alphabetically', 'library.option.sort.label.alphabetically'],
['dateAdded', 'library.option.sort.label.by_date_added'],
['lastRead', 'library.option.sort.label.by_last_read'],
['latestFetchedChapter', 'library.option.sort.label.by_latest_fetched_chapter'],
['latestUploadedChapter', 'library.option.sort.label.by_latest_uploaded_chapter'],
const SORT_OPTIONS: [LibrarySortMode, MessageDescriptor][] = [
['unreadChapters', msg`Unread chapters`],
['totalChapters', msg`Total chapters`],
['alphabetically', msg`A-Z`],
['dateAdded', msg`Recently added`],
['lastRead', msg`Recently read`],
['latestFetchedChapter', msg`Latest fetched chapter`],
['latestUploadedChapter', msg`Latest uploaded chapter`],
];
export const LibraryOptionsPanel = ({
@@ -56,21 +58,21 @@ export const LibraryOptionsPanel = ({
open: boolean;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
const categoryLibraryOptions = useGetCategoryMetadata(category);
const updateCategoryLibraryOptions = createUpdateCategoryMetadata(category, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
const {
settings: { showTabSize, showContinueReadingButton, showDownloadBadge, showUnreadBadge, gridLayout },
} = useMetadataServerSettings();
const setSettingValue = createUpdateMetadataServerSettings((e) =>
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
);
return (
@@ -84,31 +86,31 @@ export const LibraryOptionsPanel = ({
return (
<>
<ThreeStateCheckboxInput
label={t('global.filter.label.unread')}
label={t`Unread`}
checked={categoryLibraryOptions.hasUnreadChapters}
onChange={(c) => updateCategoryLibraryOptions('hasUnreadChapters', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.started')}
label={t`Started`}
checked={categoryLibraryOptions.hasReadChapters}
onChange={(c) => updateCategoryLibraryOptions('hasReadChapters', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.downloaded')}
label={t`Downloaded`}
checked={categoryLibraryOptions.hasDownloadedChapters}
onChange={(c) => updateCategoryLibraryOptions('hasDownloadedChapters', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.bookmarked')}
label={t`Bookmarked`}
checked={categoryLibraryOptions.hasBookmarkedChapters}
onChange={(c) => updateCategoryLibraryOptions('hasBookmarkedChapters', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.duplicate_chapters')}
label={t`Duplicate chapters`}
checked={categoryLibraryOptions.hasDuplicateChapters}
onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)}
/>
<FormLabel sx={{ mt: 2 }}>{t('manga.label.status')}</FormLabel>
<FormLabel sx={{ mt: 2 }}>{t`Status`}</FormLabel>
{Object.values(MangaStatus).map((status) => (
<ThreeStateCheckboxInput
key={status}
@@ -122,7 +124,7 @@ export const LibraryOptionsPanel = ({
}
/>
))}
<FormLabel sx={{ mt: 2 }}>{t('global.filter.label.tracked')}</FormLabel>
<FormLabel sx={{ mt: 2 }}>{t`Tracked`}</FormLabel>
{loggedInTrackers.map((tracker) => (
<ThreeStateCheckboxInput
key={tracker.id}
@@ -157,50 +159,47 @@ export const LibraryOptionsPanel = ({
if (key === 'display') {
return (
<>
<FormLabel>{t('global.grid_layout.title')}</FormLabel>
<FormLabel>{t`Display mode`}</FormLabel>
<RadioGroup
onChange={(e) => updateMetadataServerSettings('gridLayout', Number(e.target.value))}
value={gridLayout}
>
<RadioInput
label={t('global.grid_layout.label.compact_grid')}
label={t`Compact grid`}
value={GridLayout.Compact}
checked={gridLayout == null || gridLayout === GridLayout.Compact}
/>
<RadioInput
label={t('global.grid_layout.label.comfortable_grid')}
label={t`Comfortable grid`}
value={GridLayout.Comfortable}
checked={gridLayout === GridLayout.Comfortable}
/>
<RadioInput
label={t('global.grid_layout.label.list')}
label={t`List`}
value={GridLayout.List}
checked={gridLayout === GridLayout.List}
/>
</RadioGroup>
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.badge.title')}</FormLabel>
<FormLabel sx={{ mt: 2 }}>{t`Badges`}</FormLabel>
<CheckboxInput
label={t('library.option.display.badge.label.unread_badges')}
label={t`Unread badges`}
checked={showUnreadBadge}
onChange={() => updateMetadataServerSettings('showUnreadBadge', !showUnreadBadge)}
/>
<CheckboxInput
label={t('library.option.display.badge.label.download_badges')}
label={t`Download badges`}
checked={showDownloadBadge}
onChange={() => updateMetadataServerSettings('showDownloadBadge', !showDownloadBadge)}
/>
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.tab.title')}</FormLabel>
<FormLabel sx={{ mt: 2 }}>{t`Tabs`}</FormLabel>
<CheckboxInput
label={t('library.option.display.tab.label.show_number_of_items')}
label={t`Show number of items`}
checked={showTabSize}
onChange={() => setSettingValue('showTabSize', !showTabSize)}
/>
<FormLabel sx={{ mt: 2 }}>{t('global.label.other')}</FormLabel>
<FormLabel sx={{ mt: 2 }}>{t`Other`}</FormLabel>
<CheckboxInput
label={t('library.option.display.other.label.show_continue_reading_button')}
label={t`Show continue reading button`}
checked={showContinueReadingButton}
onChange={() =>
updateMetadataServerSettings(

View File

@@ -9,7 +9,7 @@
import FilterList from '@mui/icons-material/FilterList';
import IconButton from '@mui/material/IconButton';
import { ComponentProps, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { LibraryOptionsPanel } from '@/features/library/components/LibraryOptionsPanel.tsx';
import { getCategoryMetadata } from '@/features/category/services/CategoryMetadata.ts';
@@ -19,7 +19,7 @@ export const LibraryToolbarMenu = ({
}: {
category: ComponentProps<typeof LibraryOptionsPanel>['category'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [open, setOpen] = useState(false);
const options = getCategoryMetadata(category);
@@ -34,7 +34,7 @@ export const LibraryToolbarMenu = ({
return (
<>
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton onClick={() => setOpen(!open)} color={active ? 'warning' : 'inherit'}>
<FilterList />
</IconButton>

View File

@@ -11,10 +11,11 @@ import Tab from '@mui/material/Tab';
import { styled, useTheme } from '@mui/material/styles';
import { useCallback, useMemo, useState } from 'react';
import { useQueryParam, NumberParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import { Link } from 'react-router-dom';
import Box from '@mui/material/Box';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -59,7 +60,7 @@ const TitleSizeTag = ({ sx, ...props }: ChipProps) => (
);
export function Library() {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const {
@@ -155,7 +156,7 @@ export function Library() {
}
return (
<SelectionFAB selectedItemsCount={selectedItemIds.length} title="manga.title">
<SelectionFAB title={plural(selectedItemIds.length, { one: '# manga', other: '# manga' })}>
{(handleClose, setHideMenu) => (
<MangaActionMenuItems
selectedMangas={selectedMangas}
@@ -181,7 +182,7 @@ export function Library() {
to={AppRoutes.sources.childRoutes.searchAll.path(query)}
sx={{ textTransform: 'none', width: '100%' }}
>
{t('library.action.label.search_globally', { query })}
{t`Search for "${query}" globally`}
</Button>
</Box>
),
@@ -190,7 +191,7 @@ export function Library() {
useAppTitle(
<TitleWithSizeTag>
{t('library.title')}
{t`Library`}
{showTabSize && (
<TitleSizeTag
sx={{ ...theme.applyStyles('light', { backgroundColor: 'background.paper' }) }}
@@ -198,7 +199,7 @@ export function Library() {
/>
)}
</TitleWithSizeTag>,
t('library.title'),
t`Library`,
[t, showTabSize, librarySize],
);
useAppAction(
@@ -240,7 +241,7 @@ export function Library() {
if (tabsError != null || librarySizeResponse.error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={tabsError?.message ?? librarySizeResponse.error?.message}
retry={() => {
if (tabsError) {
@@ -260,7 +261,7 @@ export function Library() {
}
if (tabs.length === 0) {
return <EmptyViewAbsoluteCentered message={t('library.error.label.empty')} />;
return <EmptyViewAbsoluteCentered message={t`Your library is empty`} />;
}
if (tabs.length === 1) {
@@ -271,7 +272,7 @@ export function Library() {
// the key needs to include filters and query to force a re-render of the virtuoso grid to prevent https://github.com/petyosi/react-virtuoso/issues/1242
key={filterKey}
mangas={mangas}
message={mangaError ? t('manga.error.label.request_failure') : t('library.error.label.empty')}
message={mangaError ? t`Could not load manga` : t`Your library is empty`}
messageExtra={mangaError?.message}
isLoading={mangaLoading}
selectedMangaIds={selectedItemIds}
@@ -310,9 +311,7 @@ export function Library() {
// the key needs to include filters and query to force a re-render of the virtuoso grid to prevent https://github.com/petyosi/react-virtuoso/issues/1242
key={filterKey}
mangas={mangas}
message={
mangaError ? t('manga.error.label.request_failure') : t('category.error.label.empty')
}
message={mangaError ? t`Could not load manga` : t`The category is empty`}
messageExtra={mangaError?.message}
isLoading={mangaLoading}
selectedMangaIds={selectedItemIds}

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useCallback, useEffect, useMemo, useState } from 'react';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
@@ -15,6 +14,7 @@ import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useLocalStorage } from '@/base/hooks/useStorage.tsx';
import { GridLayouts } from '@/base/components/GridLayouts.tsx';
@@ -37,7 +37,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts';
export const LibraryDuplicates = () => {
const { t } = useTranslation();
const { t } = useLingui();
const [gridLayout, setGridLayout] = useLocalStorage('libraryDuplicatesGridLayout', GridLayout.List);
const [checkAlternativeTitles, setCheckAlternativeTitles] = useLocalStorage(
@@ -46,7 +46,7 @@ export const LibraryDuplicates = () => {
);
useAppTitleAndAction(
t('library.settings.advanced.duplicates.label.title'),
t`Duplicated entries`,
<>
<GridLayouts gridLayout={gridLayout} onChange={setGridLayout} />
<PopupState variant="popover" popupId="library-dupliactes-settings">
@@ -58,7 +58,7 @@ export const LibraryDuplicates = () => {
<Menu {...bindMenu(popupState)}>
<MenuItem>
<CheckboxInput
label={t('library.settings.advanced.duplicates.settings.label.check_description')}
label={t`Check description`}
checked={checkAlternativeTitles}
onChange={(_, checked) => setCheckAlternativeTitles(checked)}
/>
@@ -130,7 +130,7 @@ export const LibraryDuplicates = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('LibraryDuplicates::refetch'))}
/>

View File

@@ -6,14 +6,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import ListSubheader from '@mui/material/ListSubheader';
import { t as translate } from 'i18next';
import { useLingui } from '@lingui/react/macro';
import { plural, t as translate } from '@lingui/core/macro';
import { GlobalUpdateSettings } from '@/features/settings/components/globalUpdate/GlobalUpdateSettings.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import {
@@ -56,16 +56,16 @@ const removeNonLibraryMangasFromCategories = async (): Promise<void> => {
clearCategories: true,
}).response;
}
makeToast(translate('library.settings.advanced.database.cleanup.label.success'), 'success');
makeToast(translate`Removed non library manga from categories`, 'success');
} catch (e) {
makeToast(translate('library.settings.advanced.database.cleanup.label.error'), 'error', getErrorMessage(e));
makeToast(translate`Could not remove non library manga from categories`, 'error', getErrorMessage(e));
}
};
export function LibrarySettings() {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('library.title'));
useAppTitle(t`Library`);
const categories = requestManager.useGetCategories<GetCategoriesSettingsQuery, GetCategoriesSettingsQueryVariables>(
GET_CATEGORIES_SETTINGS,
@@ -78,7 +78,7 @@ export function LibrarySettings() {
} = useMetadataServerSettings();
const setSettingValue = createUpdateMetadataServerSettings<keyof MetadataLibrarySettings>((e) =>
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
);
// -1 for the DEFAULT category
@@ -93,7 +93,7 @@ export function LibrarySettings() {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (serverSettings.error) {
@@ -121,20 +121,23 @@ export function LibrarySettings() {
<List
subheader={
<ListSubheader component="div" id="library-category-settings">
{t('category.title.category_other')}
{t`Categories`}
</ListSubheader>
}
>
<ListItemLink to={AppRoutes.settings.childRoutes.categories.path}>
<ListItemText
primary={t('category.dialog.title.edit_category_other')}
secondary={t('category.value', { count: categoryCount })}
primary={t`Edit categories`}
secondary={plural(categoryCount, {
one: '# category',
other: '# categories',
})}
/>
</ListItemLink>
<ListItem>
<ListItemText
primary={t('library.settings.general.add_to_library.category_selection.label.title')}
secondary={t('library.settings.general.add_to_library.category_selection.label.description')}
primary={t`Category selection dialog`}
secondary={t`Show the category selection dialog when adding a manga to the library`}
/>
<Switch
edge="end"
@@ -144,10 +147,8 @@ export function LibrarySettings() {
</ListItem>
<ListItem>
<ListItemText
primary={t('library.settings.general.remove_from_library.remove_from_categories.label.title')}
secondary={t(
'library.settings.general.remove_from_library.remove_from_categories.label.description',
)}
primary={t`Forget manga categories`}
secondary={t`Remove manga from categories when removing them from the library`}
/>
<Switch
edge="end"
@@ -159,14 +160,14 @@ export function LibrarySettings() {
<List
subheader={
<ListSubheader component="div" id="library-general-settings">
{t('global.label.general')}
{t`General`}
</ListSubheader>
}
>
<ListItem>
<ListItemText
primary={t('library.settings.general.search.ignore_filters.label.title')}
secondary={t('library.settings.general.search.ignore_filters.label.description')}
primary={t`Ignore filters when searching`}
secondary={t`Search results will include manga that do not match the current filters`}
/>
<Switch
edge="end"
@@ -182,20 +183,20 @@ export function LibrarySettings() {
<List
subheader={
<ListSubheader component="div" id="library-advanced">
{t('global.label.advanced')}
{t`Advanced`}
</ListSubheader>
}
>
<ListItemButton onClick={() => removeNonLibraryMangasFromCategories()}>
<ListItemText
primary={t('library.settings.advanced.database.cleanup.label.title')}
secondary={t('library.settings.advanced.database.cleanup.label.description')}
primary={t`Cleanup database`}
secondary={t`Remove non library manga from categories`}
/>
</ListItemButton>
<ListItemLink to={AppRoutes.settings.childRoutes.library.childRoutes.duplicates.path}>
<ListItemText
primary={t('library.settings.advanced.duplicates.label.title')}
secondary={t('library.settings.advanced.duplicates.label.description')}
primary={t`Duplicated entries`}
secondary={t`Show all duplicated entries in your library`}
/>
</ListItemLink>
</List>

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<

View File

@@ -6,18 +6,18 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { SortBy, SortOrder } from '@/features/migration/Migration.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export const sortByToTranslationKey: Record<SortBy, TranslationKey> = {
[SortBy.SOURCE_NAME]: 'migrate.sort.by_source_name',
[SortBy.MANGA_COUNT]: 'migrate.sort.by_manga_count',
export const sortByToTranslation: Record<SortBy, MessageDescriptor> = {
[SortBy.SOURCE_NAME]: msg`By source name`,
[SortBy.MANGA_COUNT]: msg`By manga count`,
};
export const sortOrderToTranslationKey: Record<SortBy, TranslationKey> = {
[SortOrder.ASC]: 'global.sort.label.asc',
[SortOrder.DESC]: 'global.sort.label.desc',
export const sortOrderToTranslation: Record<SortBy, MessageDescriptor> = {
[SortOrder.ASC]: msg`Ascending`,
[SortOrder.DESC]: msg`Descending`,
};
export const DEFAULT_SORT_SETTINGS = {

View File

@@ -11,11 +11,11 @@ import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { useState } from 'react';
import FormGroup from '@mui/material/FormGroup';
import { useLingui } from '@lingui/react/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { makeToast } from '@/base/utils/Toast.ts';
@@ -29,7 +29,7 @@ import { MigrateMode } from '@/features/manga/Manga.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrateTo: number; onClose: () => void }) => {
const { t } = useTranslation();
const { t } = useLingui();
const navigate = useNavigate();
@@ -51,7 +51,7 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat
throw new Error(`MigrateDialog::migrate: unexpected mangaId "${mangaId}"`);
}
makeToast(t('migrate.label.info'), 'info');
makeToast(t`Migrating manga…`, 'info');
setIsMigrationInProcess(true);
@@ -72,30 +72,30 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat
return (
<Dialog open fullWidth onClose={onClose}>
<DialogTitle>{t('migrate.dialog.title')}</DialogTitle>
<DialogTitle>{t`Select data to include`}</DialogTitle>
<DialogContent dividers>
<FormGroup>
<CheckboxInput
disabled={isMigrationInProcess}
label={t('chapter.title_one')}
label={t`Chapter`}
checked={migrateChapters}
onChange={(_, checked) => setMigrationFlag('migrateChapters', checked)}
/>
<CheckboxInput
disabled={isMigrationInProcess}
label={t('category.title.category_one')}
label={t`Category`}
checked={migrateCategories}
onChange={(_, checked) => setMigrationFlag('migrateCategories', checked)}
/>
<CheckboxInput
disabled={isMigrationInProcess}
label={t('tracking.title')}
label={t`Tracking`}
checked={migrateTracking}
onChange={(_, checked) => setMigrationFlag('migrateTracking', checked)}
/>
<CheckboxInput
disabled={isMigrationInProcess}
label={t('migrate.dialog.label.delete_downloaded')}
label={t`Delete downloaded`}
checked={deleteChapters}
onChange={(_, checked) => setMigrationFlag('deleteChapters', checked)}
/>
@@ -114,17 +114,17 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat
component={Link}
to={AppRoutes.manga.path(mangaIdToMigrateTo)}
>
{t('migrate.dialog.action.button.show_entry')}
{t`Show entry`}
</Button>
<Stack direction="row">
<Button disabled={isMigrationInProcess} onClick={onClose}>
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button disabled={isMigrationInProcess} onClick={() => migrate('copy')}>
{t('global.button.copy')}
{t`Copy`}
</Button>
<Button disabled={isMigrationInProcess} onClick={() => migrate('migrate')}>
{t('global.button.migrate')}
{t`Migrate`}
</Button>
</Stack>
</Stack>

View File

@@ -12,7 +12,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import Chip from '@mui/material/Chip';
import Typography from '@mui/material/Typography';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
@@ -26,10 +26,10 @@ export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas'][
// TODO - cleanup source/extension components
export const MigrationCard = ({ id, name, lang, iconUrl, mangaCount }: TMigratableSource) => {
const { t } = useTranslation();
const { t } = useLingui();
const isLocalSource = Number(id) === 0;
const sourceName = isLocalSource ? t('source.local_source.title') : name;
const sourceName = isLocalSource ? t`Local source` : name;
return (
<Card>

View File

@@ -8,7 +8,7 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { TMigratableSource } from '@/features/migration/components/MigrationCard.tsx';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -25,7 +25,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts';
export const Migrate = () => {
const { t } = useTranslation();
const { t } = useLingui();
const { sourceId: paramSourceId } = useParams<{ sourceId: string }>();
@@ -68,7 +68,7 @@ export const Migrate = () => {
});
useAppTitleAndAction(
name ?? sourceId ?? t('migrate.title'),
name ?? sourceId ?? t`Migrate`,
<GridLayouts gridLayout={gridLayout} onChange={setGridLayout} />,
[gridLayout],
);
@@ -96,7 +96,7 @@ export const Migrate = () => {
const error = (hasErrorSource ? sourceError : mangasError)!;
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (hasErrorSource) {

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import List from '@mui/material/List';
import Stack from '@mui/material/Stack';
@@ -15,6 +14,7 @@ import SortByAlphaIcon from '@mui/icons-material/SortByAlpha';
import TagIcon from '@mui/icons-material/Tag';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -23,7 +23,7 @@ import { MigrationCard, TMigratableSource } from '@/features/migration/component
import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { SortBy, SortOrder, SortSettings, TMigratableSourcesResult } from '@/features/migration/Migration.types.ts';
import { sortByToTranslationKey, sortOrderToTranslationKey } from '@/features/migration/Migration.constants.ts';
import { sortByToTranslation, sortOrderToTranslation } from '@/features/migration/Migration.constants.ts';
import {
createUpdateMetadataServerSettings,
useMetadataServerSettings,
@@ -75,14 +75,14 @@ const getMigratableSources = (
};
export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
const { t } = useTranslation();
const { t } = useLingui();
const { appBarHeight } = useNavBarContext();
const {
settings: { migrateSortSettings },
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<'migrateSortSettings'>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
const { sortBy, sortOrder } = migrateSortSettings;
@@ -101,7 +101,7 @@ export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('Migration::refetch'))}
/>
@@ -123,7 +123,7 @@ export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
zIndex: 1,
}}
>
<CustomTooltip title={t(sortByToTranslationKey[sortBy])}>
<CustomTooltip title={t(sortByToTranslation[sortBy])}>
<IconButton
color="inherit"
onClick={() =>
@@ -133,7 +133,7 @@ export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
{sortBy ? <TagIcon /> : <SortByAlphaIcon />}
</IconButton>
</CustomTooltip>
<CustomTooltip title={t(sortOrderToTranslationKey[sortOrder])}>
<CustomTooltip title={t(sortOrderToTranslation[sortOrder])}>
<IconButton
color="inherit"
onClick={() =>

View File

@@ -19,7 +19,8 @@ import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import SettingsIcon from '@mui/icons-material/Settings';
import InfoIcon from '@mui/icons-material/Info';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg, plural } from '@lingui/core/macro';
import { NavbarItem, NavBarItemMoreGroup } from '@/features/navigation-bar/NavigationBar.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -30,7 +31,7 @@ type RestrictedNavBarItem<Show extends NavbarItem['show']> = Omit<NavbarItem, 's
const NAVIGATION_BAR_BASE_ITEMS = [
{
path: AppRoutes.library.path() as RestrictedNavBarItem<'both'>['path'],
title: 'library.title',
title: msg`Library`,
SelectedIconComponent: CollectionsBookmarkIcon,
IconComponent: CollectionsOutlinedBookmarkIcon,
show: 'both',
@@ -38,7 +39,7 @@ const NAVIGATION_BAR_BASE_ITEMS = [
},
{
path: AppRoutes.updates.path,
title: 'updates.title',
title: msg`Updates`,
SelectedIconComponent: NewReleasesIcon,
IconComponent: NewReleasesOutlinedIcon,
show: 'both',
@@ -46,7 +47,7 @@ const NAVIGATION_BAR_BASE_ITEMS = [
},
{
path: AppRoutes.history.path,
title: 'history.title',
title: msg`History`,
SelectedIconComponent: HistoryIcon,
IconComponent: HistoryOutlinedIcon,
show: 'both',
@@ -54,13 +55,12 @@ const NAVIGATION_BAR_BASE_ITEMS = [
},
{
path: AppRoutes.browse.path() as RestrictedNavBarItem<'both'>['path'],
title: 'global.label.browse',
title: msg`Browse`,
SelectedIconComponent: ExploreIcon,
IconComponent: ExploreOutlinedIcon,
show: 'both',
moreGroup: NavBarItemMoreGroup.GENERAL,
useBadge: () => {
const { t } = useTranslation();
const { data } = requestManager.useGetExtensionList({ fetchPolicy: 'cache-only' });
const extensions = data?.extensions.nodes ?? [];
@@ -75,7 +75,10 @@ const NAVIGATION_BAR_BASE_ITEMS = [
return {
count: availableUpdates,
title: t('extension.label.available_updates', { count: availableUpdates }),
title: plural(availableUpdates, {
one: '# update available',
other: '# updates available',
}),
};
},
},
@@ -84,14 +87,14 @@ const NAVIGATION_BAR_BASE_ITEMS = [
const NAVIGATION_BAR_DESKTOP_ITEMS = [
{
path: AppRoutes.downloads.path,
title: 'download.title.download',
moreTitle: 'download.title.queue',
title: msg`Downloads`,
moreTitle: msg`Download queue`,
SelectedIconComponent: GetAppIcon,
IconComponent: GetAppOutlinedIcon,
show: 'desktop',
moreGroup: NavBarItemMoreGroup.HIDDEN_ITEM,
useBadge: () => {
const { t } = useTranslation();
const { t } = useLingui();
const { data } = requestManager.useGetDownloadStatus();
const downloadStatus = data?.downloadStatus;
@@ -107,13 +110,13 @@ const NAVIGATION_BAR_DESKTOP_ITEMS = [
return {
count,
title: t(isPaused ? 'download.queue.info.paused' : 'download.queue.info.remaining', { count }),
title: isPaused ? t`Paused — ${count} remaining` : t`${count} remaining`,
};
},
},
{
path: AppRoutes.settings.path,
title: 'settings.title',
title: msg`Settings`,
SelectedIconComponent: SettingsIcon,
IconComponent: SettingsIcon,
show: 'desktop',
@@ -121,7 +124,7 @@ const NAVIGATION_BAR_DESKTOP_ITEMS = [
},
{
path: AppRoutes.about.path,
title: 'settings.about.title',
title: msg`About`,
SelectedIconComponent: InfoIcon,
IconComponent: InfoIcon,
show: 'desktop',
@@ -132,7 +135,7 @@ const NAVIGATION_BAR_DESKTOP_ITEMS = [
export const NAVIGATION_BAR_MOBILE_ITEMS = [
{
path: AppRoutes.more.path,
title: 'global.label.more',
title: msg`More`,
SelectedIconComponent: MoreHorizIcon,
IconComponent: MoreHorizIcon,
show: 'mobile',

View File

@@ -6,11 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { OverridableComponent } from '@mui/material/OverridableComponent';
import { SvgIconTypeMap } from '@mui/material/SvgIcon';
import { ReactNode } from 'react';
import { StaticAppRoute } from '@/base/AppRoute.constants.ts';
import { TranslationKey } from '@/base/Base.types.ts';
export interface INavbarOverride {
status: boolean;
@@ -25,8 +25,8 @@ export enum NavBarItemMoreGroup {
export interface NavbarItem {
path: StaticAppRoute;
title: TranslationKey;
moreTitle?: TranslationKey;
title: MessageDescriptor;
moreTitle?: MessageDescriptor;
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
show: 'mobile' | 'desktop' | 'both';

View File

@@ -10,7 +10,6 @@ import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import IconButton from '@mui/material/IconButton';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
@@ -22,6 +21,7 @@ import Box from '@mui/material/Box';
import ListItem from '@mui/material/ListItem';
import Badge from '@mui/material/Badge';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
@@ -40,7 +40,7 @@ const DrawerHeader = styled('div')(({ theme }) => ({
}));
const NavigationBarItem = ({ path, title, IconComponent, SelectedIconComponent, useBadge }: NavbarItem) => {
const { t } = useTranslation();
const { t } = useLingui();
const location = useLocation();
const { isCollapsed } = useNavBarContext();
const theme = useTheme();

View File

@@ -8,19 +8,19 @@
import BottomNavigation from '@mui/material/BottomNavigation';
import BottomNavigationAction from '@mui/material/BottomNavigationAction';
import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper';
import { CSSProperties, useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import Badge from '@mui/material/Badge';
import { useLingui } from '@lingui/react/macro';
import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
import { NavbarItem } from '@/features/navigation-bar/NavigationBar.types.ts';
import { StaticAppRoute } from '@/base/AppRoute.constants.ts';
export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const { setBottomBarHeight } = useNavBarContext();
const location = useLocation();

View File

@@ -6,7 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
@@ -18,18 +19,21 @@ export const ReaderSettingAutoScroll = ({
}: Pick<IReaderSettings, 'autoScroll'> & {
setAutoScroll: (updatedAutoScroll: IReaderSettings['autoScroll'], commit: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<>
<CheckboxInput
label={t('reader.settings.auto_scroll.smooth')}
label={t`Smooth auto scrolling`}
checked={autoScroll.smooth}
onChange={(_, checked) => setAutoScroll({ ...autoScroll, smooth: checked }, true)}
/>
<SliderInput
label={t('reader.settings.auto_scroll.speed')}
value={t('global.time.seconds.value', { count: autoScroll.value })}
label={t`Auto scroll speed`}
value={plural(autoScroll.value, {
one: '# second',
other: '# seconds',
})}
onDefault={() =>
setAutoScroll({ ...autoScroll, value: DEFAULT_READER_SETTINGS.autoScroll.value }, true)
}

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import PauseCircleFilledIcon from '@mui/icons-material/PauseCircleFilled';
import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled';
@@ -15,6 +14,8 @@ import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import { useRef } from 'react';
import { d } from 'koration';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { AUTO_SCROLL_SPEED } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { coerceIn } from '@/lib/HelperFunctions.ts';
@@ -26,7 +27,7 @@ export const ReaderNavBarDesktopAutoScroll = ({
}: Pick<IReaderSettings, 'autoScroll'> & {
setAutoScroll: (newAutoScroll: IReaderSettings['autoScroll'], commit: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const { isActive, toggleActive } = useReaderAutoScrollStore((state) => ({
isActive: state.autoScroll.isActive,
toggleActive: state.autoScroll.toggleActive,
@@ -44,7 +45,7 @@ export const ReaderNavBarDesktopAutoScroll = ({
variant="contained"
startIcon={isActive ? <PauseCircleFilledIcon /> : <PlayCircleFilledIcon />}
>
{t('reader.settings.auto_scroll.title')}
{t`Auto scroll`}
</Button>
<TextField
value={autoScroll.value}
@@ -73,7 +74,10 @@ export const ReaderNavBarDesktopAutoScroll = ({
inputProps: AUTO_SCROLL_SPEED,
endAdornment: (
<InputAdornment position="end">
{t('global.time.seconds.second', { count: autoScroll.value })}
{plural(autoScroll.value, {
one: 'Second',
other: 'Seconds',
})}
</InputAdornment>
),
},

View File

@@ -7,11 +7,10 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
export const ReaderSettingBrightness = ({
@@ -24,18 +23,18 @@ export const ReaderSettingBrightness = ({
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.brightness')}
label={t`Custom brightness`}
checked={brightness.enabled}
onChange={(_, checked) => updateSetting('brightness', { ...brightness, enabled: checked }, true)}
/>
{brightness.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.brightness')}
label={t`Custom brightness`}
value={brightness.value}
onDefault={() =>
updateSetting(

View File

@@ -7,7 +7,7 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
@@ -23,18 +23,18 @@ export const ReaderSettingContrast = ({
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.contrast')}
label={t`Custom contrast`}
checked={contrast.enabled}
onChange={(_, checked) => updateSetting('contrast', { ...contrast, enabled: checked }, true)}
/>
{contrast.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.contrast')}
label={t`Custom contrast`}
value={contrast.value}
onDefault={() =>
updateSetting(

View File

@@ -7,7 +7,7 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
@@ -17,15 +17,11 @@ export const ReaderSettingGrayscale = ({
}: Pick<IReaderSettings['customFilter'], 'grayscale'> & {
updateSetting: (grayscale: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.grayscale')}
checked={grayscale}
onChange={(_, checked) => updateSetting(checked)}
/>
<CheckboxInput label={t`Grayscale`} checked={grayscale} onChange={(_, checked) => updateSetting(checked)} />
</Stack>
);
};

View File

@@ -7,7 +7,7 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
@@ -23,18 +23,18 @@ export const ReaderSettingHue = ({
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.hue')}
label={t`Custom hue`}
checked={hue.enabled}
onChange={(_, checked) => updateSetting('hue', { ...hue, enabled: checked }, true)}
/>
{hue.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.hue')}
label={t`Custom hue`}
value={hue.value}
onDefault={() =>
updateSetting('hue', { ...hue, value: DEFAULT_READER_SETTINGS.customFilter.hue.value }, true)

View File

@@ -7,7 +7,7 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
@@ -17,15 +17,11 @@ export const ReaderSettingInvert = ({
}: Pick<IReaderSettings['customFilter'], 'invert'> & {
updateSetting: (invert: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.invert')}
checked={invert}
onChange={(_, checked) => updateSetting(checked)}
/>
<CheckboxInput label={t`Invert`} checked={invert} onChange={(_, checked) => updateSetting(checked)} />
</Stack>
);
};

View File

@@ -6,28 +6,29 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
import {
CUSTOM_FILTER,
DEFAULT_READER_SETTINGS,
READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA,
READER_BLEND_MODE_VALUES,
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { ButtonSelectInput } from '@/base/components/inputs/ButtonSelectInput.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
type RGBAType = Exclude<keyof IReaderSettings['customFilter']['rgba']['value'], 'blendMode'>;
const RGBA_TYPE_TO_TRANSLATION_KEY: Record<RGBAType, TranslationKey> = {
red: 'reader.settings.custom_filter.rgba.red',
green: 'reader.settings.custom_filter.rgba.green',
blue: 'reader.settings.custom_filter.rgba.blue',
alpha: 'reader.settings.custom_filter.rgba.alpha',
const RGBA_TYPE_TO_TRANSLATION: Record<RGBAType, MessageDescriptor> = {
red: msg`Red`,
green: msg`Green`,
blue: msg`Blue`,
alpha: msg`Alpha`,
};
export const ReaderSettingRGBA = ({
@@ -40,12 +41,12 @@ export const ReaderSettingRGBA = ({
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.rgba.title')}
label={t`Custom color filters`}
checked={rgba.enabled}
onChange={(_, checked) => updateSetting('rgba', { ...rgba, enabled: checked }, true)}
/>
@@ -59,7 +60,7 @@ export const ReaderSettingRGBA = ({
return (
<SliderInput
key={key}
label={t(RGBA_TYPE_TO_TRANSLATION_KEY[key as RGBAType])}
label={t(RGBA_TYPE_TO_TRANSLATION[key as RGBAType])}
value={value}
onDefault={() =>
updateSetting(
@@ -101,7 +102,7 @@ export const ReaderSettingRGBA = ({
);
})}
<ButtonSelectInput
label={t('reader.settings.custom_filter.rgba.blend_mode.title')}
label={t`Color filter blend mode`}
value={rgba.value.blendMode}
values={READER_BLEND_MODE_VALUES}
setValue={(value) =>

View File

@@ -7,7 +7,7 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
@@ -23,18 +23,18 @@ export const ReaderSettingSaturate = ({
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.saturate')}
label={t`Custom saturate`}
checked={saturate.enabled}
onChange={(_, checked) => updateSetting('saturate', { ...saturate, enabled: checked }, true)}
/>
{saturate.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.saturate')}
label={t`Custom saturate`}
value={saturate.value}
onDefault={() =>
updateSetting(

View File

@@ -7,7 +7,7 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
@@ -17,15 +17,11 @@ export const ReaderSettingSepia = ({
}: Pick<IReaderSettings['customFilter'], 'sepia'> & {
updateSetting: (sepia: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.sepia')}
checked={sepia}
onChange={(_, checked) => updateSetting(checked)}
/>
<CheckboxInput label={t`Sepia`} checked={sepia} onChange={(_, checked) => updateSetting(checked)} />
</Stack>
);
};

View File

@@ -6,14 +6,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { d } from 'koration';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/features/reader/Reader.types.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { READING_MODE_VALUE_TO_DISPLAY_DATA } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
const HIDE_PREVIEW_TIMEOUT = d(5).seconds.inWholeMilliseconds;
@@ -28,7 +28,7 @@ export const useReaderShowSettingPreviewOnChange = (
shouldShowTapZoneLayoutPreview: IReaderSettings['shouldShowTapZoneLayoutPreview'],
setShowPreview: TReaderTapZoneContext['setShowPreview'],
) => {
const { t } = useTranslation();
const { t } = useLingui();
// show setting previews on change or when open reader
const previousReadingMode = useRef<IReaderSettingsWithDefaultFlag['readingMode']>(undefined);
@@ -44,7 +44,7 @@ export const useReaderShowSettingPreviewOnChange = (
const didReadingModeChange = JSON.stringify(readingMode) !== JSON.stringify(previousReadingMode.current);
const showReadingModePreview = shouldShowReadingModePreview && didReadingModeChange;
if (showReadingModePreview) {
makeToast(t(READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].title as TranslationKey), {
makeToast(t(READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].title as MessageDescriptor), {
autoHideDuration: HIDE_PREVIEW_TIMEOUT,
});
}

View File

@@ -7,20 +7,20 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { ReaderSettingsTypeProps } from '@/features/reader/Reader.types.ts';
import { ReaderSettingHotkey } from '@/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx';
import { READER_HOTKEYS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { ResetButton } from '@/base/components/buttons/ResetButton.tsx';
export const ReaderHotkeysSettings = ({ settings, updateSetting, onDefault }: ReaderSettingsTypeProps) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ gap: 2 }}>
<Stack sx={{ alignItems: 'end' }}>
<Typography variant="caption">{t('hotkeys.info.delete')}</Typography>
<Typography variant="caption">{t`Click key to remove binding`}</Typography>
</Stack>
{READER_HOTKEYS.map((hotkey) => (
<ReaderSettingHotkey

View File

@@ -8,18 +8,18 @@
import { Fragment } from 'react';
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Kbd } from '@/base/components/texts/Kbd.tsx';
export const Hotkey = ({ keys, removeKey }: { keys: string[]; removeKey?: (key: string) => void }) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
{keys.map((key, index) => (
<Fragment key={key}>
<CustomTooltip title={t('global.button.delete')} hidden={!removeKey}>
<CustomTooltip title={t`Delete`} hidden={!removeKey}>
<Stack
sx={{
flexDirection: 'row',

View File

@@ -6,37 +6,38 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import AddIcon from '@mui/icons-material/Add';
import Typography from '@mui/material/Typography';
import { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
import IconButton from '@mui/material/IconButton';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ReaderHotkey } from '@/features/reader/Reader.types.ts';
import { DEFAULT_READER_SETTINGS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { RecordHotkey } from '@/features/reader/hotkeys/settings/components/RecordHotkey.tsx';
import { Hotkey } from '@/features/reader/hotkeys/settings/components/Hotkey.tsx';
import { ResetButton } from '@/base/components/buttons/ResetButton.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
const READER_HOTKEY_TO_TITLE: Record<ReaderHotkey, TranslationKey> = {
[ReaderHotkey.PREVIOUS_PAGE]: 'reader.settings.hotkey.previous_page',
[ReaderHotkey.NEXT_PAGE]: 'reader.settings.hotkey.next_page',
[ReaderHotkey.SCROLL_BACKWARD]: 'reader.settings.hotkey.scroll_backward',
[ReaderHotkey.SCROLL_FORWARD]: 'reader.settings.hotkey.scroll_forward',
[ReaderHotkey.PREVIOUS_CHAPTER]: 'reader.settings.hotkey.previous_chapter',
[ReaderHotkey.NEXT_CHAPTER]: 'reader.settings.hotkey.next_chapter',
[ReaderHotkey.TOGGLE_MENU]: 'reader.settings.hotkey.menu',
[ReaderHotkey.CYCLE_SCALE_TYPE]: 'reader.settings.hotkey.scale_type',
[ReaderHotkey.STRETCH_IMAGE]: 'reader.settings.hotkey.stretch_image',
[ReaderHotkey.OFFSET_SPREAD_PAGES]: 'reader.settings.hotkey.offset_spread_pages',
[ReaderHotkey.CYCLE_READING_MODE]: 'reader.settings.hotkey.reading_mode',
[ReaderHotkey.CYCLE_READING_DIRECTION]: 'reader.settings.hotkey.reading_direction',
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: 'reader.settings.hotkey.auto_scroll',
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: 'reader.settings.hotkey.auto_scroll_speed_increase',
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: 'reader.settings.hotkey.auto_scroll_speed_decrease',
[ReaderHotkey.EXIT_READER]: 'reader.button.exit',
const READER_HOTKEY_TO_TITLE: Record<ReaderHotkey, MessageDescriptor> = {
[ReaderHotkey.PREVIOUS_PAGE]: msg`Previous page`,
[ReaderHotkey.NEXT_PAGE]: msg`Next page`,
[ReaderHotkey.SCROLL_BACKWARD]: msg`Scroll backward`,
[ReaderHotkey.SCROLL_FORWARD]: msg`Scroll forward`,
[ReaderHotkey.PREVIOUS_CHAPTER]: msg`Previous chapter`,
[ReaderHotkey.NEXT_CHAPTER]: msg`Next chapter`,
[ReaderHotkey.TOGGLE_MENU]: msg`Toggle menu`,
[ReaderHotkey.CYCLE_SCALE_TYPE]: msg`Cycle image scale type`,
[ReaderHotkey.STRETCH_IMAGE]: msg`Toggle stretch image`,
[ReaderHotkey.OFFSET_SPREAD_PAGES]: msg`Toggle offset spread pages`,
[ReaderHotkey.CYCLE_READING_MODE]: msg`Cycle reading mode`,
[ReaderHotkey.CYCLE_READING_DIRECTION]: msg`Cycle reading direction`,
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: msg`Toggle auto scroll`,
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: msg`Increase auto scroll speed`,
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: msg`Decrease auto scroll speed`,
[ReaderHotkey.EXIT_READER]: msg`Exit reader`,
};
export const ReaderSettingHotkey = ({
@@ -50,7 +51,7 @@ export const ReaderSettingHotkey = ({
existingKeys: string[];
updateSetting: (keys: string[]) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const popupState = usePopupState({ popupId: 'reader-setting-record-hotkey', variant: 'dialog' });
return (
@@ -61,7 +62,7 @@ export const ReaderSettingHotkey = ({
keys={keys}
removeKey={(keyToRemove) => updateSetting(keys.filter((key) => key !== keyToRemove))}
/>
<CustomTooltip title={t('global.button.add')}>
<CustomTooltip title={t`Add`}>
<IconButton {...bindTrigger(popupState)} color="inherit">
<AddIcon />
</IconButton>

View File

@@ -8,7 +8,6 @@
import { useEffect } from 'react';
import Stack from '@mui/material/Stack';
import { Trans, useTranslation } from 'react-i18next';
import { useRecordHotkeys } from 'react-hotkeys-hook';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -16,6 +15,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { Trans, useLingui } from '@lingui/react/macro';
import { Hotkey } from '@/features/reader/hotkeys/settings/components/Hotkey.tsx';
export const RecordHotkey = ({
@@ -27,7 +27,7 @@ export const RecordHotkey = ({
onCreate: (keys: string[]) => void;
existingKeys: string[];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [recordedKeys, { start, stop, resetKeys }] = useRecordHotkeys();
const keys = [[...recordedKeys].join('+')];
@@ -45,23 +45,15 @@ export const RecordHotkey = ({
return (
<Dialog open onClose={onClose} fullWidth>
<DialogTitle>{t('hotkeys.create.dialog.title')}</DialogTitle>
<DialogTitle>{t`Record keybind`}</DialogTitle>
<DialogContent>
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
<Trans
i18nKey="hotkeys.create.dialog.label"
components={{
Keys: recordedKeys.size ? (
<Hotkey keys={keys} />
) : (
<Typography>{t('hotkeys.create.dialog.placeholder')}</Typography>
),
}}
>
Recorded keys:
<Trans>
Recorded hotkeys:{' '}
{recordedKeys.size ? <Hotkey keys={keys} /> : <Typography>{t`Press keys`}</Typography>}
</Trans>
</Stack>
{isExistingKey && <Typography color="error">{t('hotkeys.create.error.exists')}</Typography>}
{isExistingKey && <Typography color="error">{t`Hotkey already exists`}</Typography>}
</DialogContent>
<DialogActions>
<Stack
@@ -71,9 +63,9 @@ export const RecordHotkey = ({
width: '100%',
}}
>
<Button onClick={resetKeys}>{t('global.button.reset')}</Button>
<Button onClick={resetKeys}>{t`Reset`}</Button>
<Stack direction="row">
<Button onClick={onClose}>{t('global.button.cancel')}</Button>
<Button onClick={onClose}>{t`Cancel`}</Button>
<Button
disabled={isExistingKey}
onClick={() => {
@@ -81,7 +73,7 @@ export const RecordHotkey = ({
onCreate(keys);
}}
>
{t('global.button.create')}
{t`Create`}
</Button>
</Stack>
</Stack>

View File

@@ -11,13 +11,13 @@ import Stack from '@mui/material/Stack';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { bindMenu, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next';
import Menu from '@mui/material/Menu';
import Link from '@mui/material/Link';
import { Link as RouterLink } from 'react-router-dom';
import { alpha } from '@mui/material/styles';
import Slide from '@mui/material/Slide';
import { memo, Ref } from 'react';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
@@ -39,7 +39,7 @@ import {
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & { ref?: Ref<HTMLDivElement> }) => {
const { t } = useTranslation();
const { t } = useLingui();
const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' });
const currentChapter = useReaderChaptersStore((state) => state.chapters.currentChapter);
@@ -102,7 +102,7 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
rel="noreferrer"
target="_blank"
>
{t('global.button.open_browser')}
{t`Open in browser`}
</MenuItem>
<MenuItem
component={Link}
@@ -111,16 +111,16 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
rel="noreferrer"
target="_blank"
>
{t('global.button.open_webview')}
{t`Open in WebView`}
</MenuItem>
<MenuItem
disabled={!realUrl}
onClick={async () => {
await navigator.clipboard.writeText(title);
makeToast(t('global.label.copied_clipboard'), 'info');
makeToast(t`Copied to clipboard`, 'info');
}}
>
{t('global.label.share')}
{t`Share`}
</MenuItem>
</Menu>
</Stack>

View File

@@ -7,17 +7,17 @@
*/
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import { memo } from 'react';
import BookmarkIcon from '@mui/icons-material/Bookmark';
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { ChapterAction, TChapterReader } from '@/features/chapter/Chapter.types.ts';
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
const BaseReaderBookmarkButton = ({ id, isBookmarked }: Pick<TChapterReader, 'id' | 'isBookmarked'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const bookmarkAction: Extract<ChapterAction, 'unbookmark' | 'bookmark'> = isBookmarked ? 'unbookmark' : 'bookmark';

View File

@@ -7,21 +7,21 @@
*/
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import ArrowBack from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { memo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
const BaseReaderExitButton = ({ exit }: { exit: ReturnType<typeof ReaderService.useExit> }) => {
const { t } = useTranslation();
const { t } = useLingui();
const getOptionForDirection = useGetOptionForDirection();
return (
<CustomTooltip title={t('reader.button.exit')}>
<CustomTooltip title={t`Exit reader`}>
<IconButton sx={{ marginRight: 2 }} onClick={exit} color="inherit">
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
</IconButton>

View File

@@ -7,10 +7,10 @@
*/
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import { memo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx';
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
@@ -27,13 +27,11 @@ export const ReaderLibraryButton = memo(() => {
const { inLibrary } = manga ?? ACTION_FALLBACK_MANGA;
const { t } = useTranslation();
const { t } = useLingui();
const { updateLibraryState } = useManageMangaLibraryState(manga ?? ACTION_FALLBACK_MANGA, true);
return (
<CustomTooltip
title={inLibrary ? t('manga.action.library.remove.label.action') : t('manga.button.add_to_library')}
>
<CustomTooltip title={inLibrary ? t`Remove from the library` : t`Add To Library`}>
<IconButton onClick={updateLibraryState} color="inherit">
{inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
</IconButton>

View File

@@ -7,13 +7,13 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import IconButton from '@mui/material/IconButton';
import PushPinIcon from '@mui/icons-material/PushPin';
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
import Divider from '@mui/material/Divider';
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
import Drawer from '@mui/material/Drawer';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ReaderNavBarDesktopProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
import { ReaderNavContainer } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavContainer.tsx';
@@ -58,7 +58,7 @@ const BaseReaderNavBarDesktop = ({
openSettings,
setReaderNavBarWidth,
}: ReaderNavBarDesktopProps & Pick<NavbarContextType, 'setReaderNavBarWidth'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const manga = useReaderStore((state) => state.manga);
const {
chapters,
@@ -112,7 +112,7 @@ const BaseReaderNavBarDesktop = ({
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<ReaderExitButton />
<CustomTooltip title={t('reader.settings.label.static_navigation')}>
<CustomTooltip title={t`Static navigation`}>
<IconButton
onClick={() => {
setReaderNavBarWidth(0);

View File

@@ -7,12 +7,12 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import IconButton from '@mui/material/IconButton';
import DownloadIcon from '@mui/icons-material/Download';
import ReplayIcon from '@mui/icons-material/Replay';
import { memo, useMemo, useRef } from 'react';
import DeleteIcon from '@mui/icons-material/Delete';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
@@ -26,7 +26,7 @@ import { useReaderChaptersStore, useReaderPagesStore } from '@/features/reader/s
import { ChapterDownloadInfo, ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
const DownloadButton = ({ id = -1, isDownloaded }: ChapterIdInfo & ChapterDownloadInfo) => {
const { t } = useTranslation();
const { t } = useLingui();
const downloadStatus = Chapters.useDownloadStatusFromCache(id);
@@ -67,7 +67,7 @@ export const ReaderNavBarDesktopActions = memo(() => {
realUrl: state.chapters.currentChapter?.realUrl ?? FALLBACK_CHAPTER.realUrl,
}));
const { t } = useTranslation();
const { t } = useLingui();
const { pageLoadStates, setPageLoadStates, setRetryFailedPagesKeyPrefix } = useReaderPagesStore((state) => ({
pageLoadStates: state.pages.pageLoadStates,
setPageLoadStates: state.pages.setPageLoadStates,
@@ -85,7 +85,7 @@ export const ReaderNavBarDesktopActions = memo(() => {
<Stack sx={{ flexDirection: 'row', justifyContent: 'center', gap: 1 }}>
<ReaderLibraryButton />
<ReaderBookmarkButton id={id} isBookmarked={isBookmarked} />
<CustomTooltip title={t('reader.button.retry_load_pages')} disabled={!haveSomePagesFailedToLoad}>
<CustomTooltip title={t`Retry errored pages`} disabled={!haveSomePagesFailedToLoad}>
<IconButton
onClick={() => {
setPageLoadStates((statePageLoadStates) =>
@@ -104,12 +104,12 @@ export const ReaderNavBarDesktopActions = memo(() => {
</IconButton>
</CustomTooltip>
<DownloadButton id={id} isDownloaded={isDownloaded} />
<CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}>
<CustomTooltip title={t`Open in browser`} disabled={!realUrl}>
<IconButton disabled={!realUrl} href={realUrl ?? ''} rel="noreferrer" target="_blank" color="inherit">
<IconBrowser />
</IconButton>
</CustomTooltip>
<CustomTooltip title={t('global.button.open_webview')} disabled={!realUrl}>
<CustomTooltip title={t`Open in WebView`} disabled={!realUrl}>
<IconButton
disabled={!realUrl}
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}

View File

@@ -7,7 +7,6 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import MenuItem from '@mui/material/MenuItem';
import { memo, useLayoutEffect } from 'react';
@@ -16,6 +15,7 @@ import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import { Link } from 'react-router-dom';
import { useLingui } from '@lingui/react/macro';
import { Select } from '@/base/components/inputs/Select.tsx';
import { ReaderChapterList } from '@/features/reader/overlay/navigation/components/ReaderChapterList.tsx';
import { ReaderNavBarDesktopNextPreviousButton } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopNextPreviousButton.tsx';
@@ -41,7 +41,7 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
} & Pick<ReaderStateChapters, 'chapters' | 'previousChapter' | 'nextChapter'> & {
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' });
@@ -53,27 +53,21 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
<ReaderNavBarDesktopNextPreviousButton
type="previous"
title={t(
getOptionForDirection(
'reader.button.previous_chapter',
'reader.button.next_chapter',
readerThemeDirection,
),
)}
title={getOptionForDirection(t`Previous chapter`, t`Next chapter`, readerThemeDirection)}
onClick={() => {
ReaderControls.openChapter(getOptionForDirection('previous', 'next', readerThemeDirection));
}}
disabled={getOptionForDirection(!previousChapter, !nextChapter, readerThemeDirection)}
/>
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t('chapter.title_one')}</InputLabel>
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t`Chapter`}</InputLabel>
<Select
{...bindTrigger(popupState)}
open={popupState.isOpen}
value={currentChapterId ?? 0}
// hide actual select menu
MenuProps={{ sx: { visibility: 'hidden' } }}
label={t('chapter.title_one')}
label={t`Chapter`}
labelId="reader-nav-bar-desktop-chapter-select"
>
{/* hacky way to use the select component with a custom menu, the only possible value that is needed is the current chapter */}
@@ -85,13 +79,7 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
<ReaderNavBarDesktopNextPreviousButton
component={Link}
type="next"
title={t(
getOptionForDirection(
'reader.button.next_chapter',
'reader.button.previous_chapter',
readerThemeDirection,
),
)}
title={getOptionForDirection(t`Next chapter`, t`Previous chapter`, readerThemeDirection)}
onClick={() => {
ReaderControls.openChapter(getOptionForDirection('next', 'previous', readerThemeDirection));
}}

View File

@@ -7,11 +7,11 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import MenuItem from '@mui/material/MenuItem';
import { memo, useMemo } from 'react';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import { useLingui } from '@lingui/react/macro';
import { Select } from '@/base/components/inputs/Select.tsx';
import { getNextIndexFromPage, getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
@@ -21,7 +21,7 @@ import { useReaderPagesStore, useReaderSettingsStore } from '@/features/reader/s
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
const BaseReaderNavBarDesktopPageNavigation = () => {
const { t } = useTranslation();
const { t } = useLingui();
const getOptionForDirection = useGetOptionForDirection();
const { currentPageIndex, pages } = useReaderPagesStore((state) => ({
currentPageIndex: state.pages.currentPageIndex,
@@ -36,7 +36,7 @@ const BaseReaderNavBarDesktopPageNavigation = () => {
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
<ReaderNavBarDesktopNextPreviousButton
type="previous"
title={t(getOptionForDirection('reader.button.previous_page', 'reader.button.next_page', direction))}
title={getOptionForDirection(t`Previous page`, t`Next page`, direction)}
disabled={getOptionForDirection(
!currentPage.primary.index,
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
@@ -45,10 +45,10 @@ const BaseReaderNavBarDesktopPageNavigation = () => {
onClick={() => ReaderControls.openPage('previous', undefined, false)}
/>
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
<InputLabel id="reader-nav-bar-desktop-page-select">{t('reader.page_info.label.page')}</InputLabel>
<InputLabel id="reader-nav-bar-desktop-page-select">{t`Page`}</InputLabel>
<Select
labelId="reader-nav-bar-desktop-page-select"
label={t('reader.page_info.label.page')}
label={t`Page`}
value={getNextIndexFromPage(currentPage)}
onChange={(e) => ReaderControls.openPage(e.target.value as number, undefined, false)}
>
@@ -61,7 +61,7 @@ const BaseReaderNavBarDesktopPageNavigation = () => {
</FormControl>
<ReaderNavBarDesktopNextPreviousButton
type="next"
title={t(getOptionForDirection('reader.button.next_page', 'reader.button.previous_page', direction))}
title={getOptionForDirection(t`Next page`, t`Previous page`, direction)}
disabled={getOptionForDirection(
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
!currentPage.primary.index,

View File

@@ -7,10 +7,10 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import SettingsIcon from '@mui/icons-material/Settings';
import { memo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { ReaderNavBarDesktopPageScale } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopPageScale.tsx';
import { ReaderNavBarDesktopReadingMode } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopReadingMode.tsx';
import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopOffsetDoubleSpread.tsx';
@@ -21,7 +21,7 @@ import { ReaderNavBarDesktopAutoScroll } from '@/features/reader/auto-scroll/set
import { useReaderSettingsStore } from '@/features/reader/stores/ReaderStore.ts';
const BaseReaderNavBarDesktopQuickSettings = ({ openSettings }: Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldStretchPage, readingDirection, autoScroll } =
useReaderSettingsStore((state) => ({
readingMode: state.settings.readingMode,
@@ -71,7 +71,7 @@ const BaseReaderNavBarDesktopQuickSettings = ({ openSettings }: Pick<ReaderNavBa
variant="contained"
startIcon={<SettingsIcon />}
>
{t('settings.title')}
{t`Settings`}
</Button>
</Stack>
);

Some files were not shown because too many files have changed in this diff Show More