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,10 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { AssertionError } from 'assert';
export function assertIsDefined<T>(value: T | undefined): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new AssertionError({ message: 'Value is undefined or null' });
throw new Error('Value is undefined or null');
}
}

View File

@@ -6,8 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { ReactNode } from 'react';
import { ParseKeys } from 'i18next';
export enum GridLayout {
Compact = 0,
@@ -20,11 +20,9 @@ export enum DirectionOffset {
NEXT = 1,
}
export type TranslationKey = ParseKeys;
interface DisplayDataTranslationKey {
interface DisplayDataTranslation {
isTitleString?: never;
title: TranslationKey;
title: MessageDescriptor;
icon: ReactNode;
}
@@ -34,7 +32,7 @@ interface DisplayDataString {
icon: ReactNode;
}
type DisplayData = DisplayDataTranslationKey | DisplayDataString;
type DisplayData = DisplayDataTranslation | DisplayDataString;
export type ValueToDisplayData<Value extends string | number> = Record<Value, DisplayData>;

View File

@@ -7,7 +7,7 @@
*/
import Checkbox from '@mui/material/Checkbox';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
export const SelectableCollectionSelectAll = ({
@@ -19,10 +19,10 @@ export const SelectableCollectionSelectAll = ({
areNoItemsSelected: boolean;
onChange: (checked: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<CustomTooltip title={t(!areAllItemsSelected ? 'global.button.select_all' : 'global.button.clear_selection')}>
<CustomTooltip title={!areAllItemsSelected ? t`Select all` : t`Clear`}>
<Checkbox
sx={{
padding: '8px',

View File

@@ -7,8 +7,8 @@
*/
import Checkbox from '@mui/material/Checkbox';
import { useTranslation } from 'react-i18next';
import ClearIcon from '@mui/icons-material/Clear';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SelectableCollectionSelectAll } from '@/base/collection/components/SelectableCollectionSelectAll.tsx';
@@ -25,7 +25,7 @@ export const SelectableCollectionSelectMode = ({
onSelectAll: (selectAll: boolean) => void;
onModeChange: (checked: boolean) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<>
@@ -36,7 +36,7 @@ export const SelectableCollectionSelectMode = ({
onChange={onSelectAll}
/>
)}
<CustomTooltip title={t(!isActive ? 'global.button.select_all' : 'global.button.cancel')}>
<CustomTooltip title={!isActive ? t`Select all` : t`Cancel`}>
<Checkbox
checkedIcon={<ClearIcon />}
sx={{

View File

@@ -11,17 +11,13 @@ import Fab from '@mui/material/Fab';
import Box from '@mui/material/Box';
import { styled } from '@mui/material/styles';
import React, { type JSX } from 'react';
import { useTranslation } from 'react-i18next';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import { DEFAULT_FAB_STYLE } from '@/base/components/buttons/StyledFab.tsx';
import { Menu } from '@/base/components/menu/Menu.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
interface SelectionFABProps {
children: (handleClose: () => void, setHideMenu: (hide: boolean) => void) => JSX.Element;
selectedItemsCount: number;
title: TranslationKey;
title: string;
}
const FabContainer = styled(Box)(({ theme }) => ({
@@ -34,32 +30,28 @@ const FabContainer = styled(Box)(({ theme }) => ({
},
}));
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, selectedItemsCount, title }) => {
const { t } = useTranslation();
return (
<PopupState variant="popover" popupId="selection-fab-menu">
{(popupState) => (
<>
<FabContainer {...bindTrigger(popupState)}>
<Fab variant="extended" color="primary" id="selectionMenuButton">
{`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`}
<MoreHoriz sx={{ ml: 1 }} />
</Fab>
</FabContainer>
<Menu
{...bindMenu(popupState)}
id="selectionMenu"
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
transformOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'selectionMenuButton',
}}
>
{(onClose, setHideMenu) => children(onClose, setHideMenu)}
</Menu>
</>
)}
</PopupState>
);
};
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, title }) => (
<PopupState variant="popover" popupId="selection-fab-menu">
{(popupState) => (
<>
<FabContainer {...bindTrigger(popupState)}>
<Fab variant="extended" color="primary" id="selectionMenuButton">
{title}
<MoreHoriz sx={{ ml: 1 }} />
</Fab>
</FabContainer>
<Menu
{...bindMenu(popupState)}
id="selectionMenu"
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
transformOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'selectionMenuButton',
}}
>
{(onClose, setHideMenu) => children(onClose, setHideMenu)}
</Menu>
</>
)}
</PopupState>
);

View File

@@ -10,10 +10,10 @@ import React, { useState } from 'react';
import SearchIcon from '@mui/icons-material/Search';
import IconButton from '@mui/material/IconButton';
import { useQueryParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SearchTextField } from '@/base/components/inputs/SearchTextField.tsx';
import { SearchParam } from '@/base/Base.types.ts';
@@ -26,7 +26,7 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
const { isClosable = true } = props;
const theme = useTheme();
const { t } = useTranslation();
const { t } = useLingui();
const [prevLocationKey, setPrevLocationKey] = useState<string>();
const location = useLocation();
@@ -119,7 +119,7 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
}
return (
<CustomTooltip title={t('search.title.search')}>
<CustomTooltip title={t`Search`}>
<IconButton onClick={() => updateSearchOpenState(true)} color="inherit">
<SearchIcon />
</IconButton>

View File

@@ -13,9 +13,8 @@ import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import React from 'react';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { GridLayout } from '@/base/Base.types.ts';
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
@@ -26,7 +25,7 @@ export function GridLayouts({
gridLayout: GridLayout;
onChange: (gridLayout: GridLayout) => void;
}) {
const { t } = useTranslation();
const { t } = useLingui();
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
@@ -43,7 +42,7 @@ export function GridLayouts({
return (
<>
<CustomTooltip title={t('global.label.display')}>
<CustomTooltip title={t`Display`}>
<IconButton
onClick={handleClick}
size="small"
@@ -64,7 +63,7 @@ export function GridLayouts({
>
<MenuItem onClick={handleClose}>
<FormControlLabel
label={t('global.grid_layout.label.compact_grid')}
label={t`Compact grid`}
value={GridLayout.Compact}
control={
<Radio
@@ -77,7 +76,7 @@ export function GridLayouts({
</MenuItem>
<MenuItem onClick={handleClose}>
<FormControlLabel
label={t('global.grid_layout.label.comfortable_grid')}
label={t`Comfortable grid`}
control={
<Radio
name={GridLayout.Comfortable.toString()}
@@ -89,7 +88,7 @@ export function GridLayouts({
</MenuItem>
<MenuItem onClick={handleClose}>
<FormControlLabel
label={t('global.grid_layout.label.list')}
label={t`List`}
control={
<Radio
name={GridLayout.List.toString()}

View File

@@ -13,9 +13,9 @@ import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useTranslation } from 'react-i18next';
import ImageIcon from '@mui/icons-material/Image';
import { SxProps, Theme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { ImageRequest, requestManager } from '@/lib/requests/RequestManager.ts';
import { Priority } from '@/lib/Queue.ts';
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
@@ -65,7 +65,7 @@ export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
retryKeyPrefix,
} = props;
const { t } = useTranslation();
const { t } = useLingui();
const loadingIndicatorRef = useRef<HTMLDivElement | null>(null);
@@ -174,7 +174,6 @@ export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
draggable={false}
/>
)}
{(!!isLoading || (src && !imageSourceUrl) || hasError) && (
<Stack
ref={loadingIndicatorRef}
@@ -205,7 +204,7 @@ export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
}}
size={small ? 'small' : 'large'}
>
{small ? <RefreshIcon /> : t('global.button.retry')}
{small ? <RefreshIcon /> : t`Retry`}
</Button>
</>
)}

View File

@@ -7,8 +7,8 @@
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { MultiValueButtonProps } from '@/base/Base.types.ts';
import { Superscript } from '@/base/components/texts/Superscript.tsx';
@@ -22,13 +22,13 @@ export const ButtonSelect = <Value extends string | number>({
isDefaultable,
onDefault,
}: MultiValueButtonProps<Value>) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
{isDefaultable && (
<Button key="default" onClick={onDefault} variant={value === undefined ? 'contained' : 'outlined'}>
{t('global.label.default')}
{t`Default`}
</Button>
)}
{values.map((displayValue) => {
@@ -39,13 +39,13 @@ export const ButtonSelect = <Value extends string | number>({
: t(valueToDisplayData[displayValue].title);
return (
<CustomTooltip key={displayValue} title={isDefault ? t('reader.settings.active_setting') : ''}>
<CustomTooltip key={displayValue} title={isDefault ? t`Active setting` : ''}>
<Button
onClick={() => setValue(displayValue)}
variant={displayValue === value ? 'contained' : 'outlined'}
startIcon={valueToDisplayData[displayValue].icon}
>
{isDefault ? <Superscript i18nKey="global.label.footnote" value={text} /> : text}
{isDefault ? <Superscript superscript="*" text={text} /> : text}
</Button>
</CustomTooltip>
);

View File

@@ -7,9 +7,9 @@
*/
import Button, { ButtonProps } from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
type PropsIconButton = { asIconButton: true } & IconButtonProps;
@@ -17,11 +17,11 @@ type PropsButton = { asIconButton?: false } & ButtonProps;
type Props = PropsIconButton | PropsButton;
export const ResetButton = ({ asIconButton, ...props }: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
if (asIconButton) {
return (
<CustomTooltip title={t('global.button.reset')}>
<CustomTooltip title={t`Reset`}>
<IconButton color="inherit" {...props}>
<RestartAltIcon />
</IconButton>
@@ -31,7 +31,7 @@ export const ResetButton = ({ asIconButton, ...props }: Props) => {
return (
<Button startIcon={<RestartAltIcon />} {...(props as ButtonProps)}>
{t('global.button.reset')}
{t`Reset`}
</Button>
);
};

View File

@@ -6,9 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { ReactNode, useMemo } from 'react';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { MultiValueButtonProps } from '@/base/Base.types.ts';
import { getNextRotationValue } from '@/base/utils/ValueRotationButton.utils.ts';
@@ -25,7 +25,7 @@ export const ValueRotationButton = <Value extends string | number>({
onDefault,
defaultIcon,
}: MultiValueButtonProps<Value> & { defaultIcon?: ReactNode }) => {
const { t } = useTranslation();
const { t } = useLingui();
const isDefault = value === undefined;
const indexOfValue = useMemo(() => {
@@ -47,11 +47,11 @@ export const ValueRotationButton = <Value extends string | number>({
size="large"
>
{defaultValue === undefined ? (
t('global.label.default')
t`Default`
) : (
<Superscript
i18nKey="settings.default_value"
value={
superscript={`(${t`Default`})`}
text={
valueToDisplayData[defaultValue].isTitleString
? valueToDisplayData[defaultValue].title
: t(valueToDisplayData[defaultValue].title)

View File

@@ -6,24 +6,25 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
import { TranslationKey } from '@/base/Base.types.ts';
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
DOWNLOADING: 'download.state.label.downloading',
ERROR: 'download.state.label.error',
FINISHED: 'download.state.label.finished',
QUEUED: 'download.state.label.queued',
const DOWNLOAD_STATE_TO_TRANSLATION_MAP: { [state in DownloadState]: MessageDescriptor } = {
DOWNLOADING: msg`Downloading`,
ERROR: msg`Error`,
FINISHED: msg`Finished`,
QUEUED: msg`Queued`,
} as const;
export const DownloadStateIndicator = ({ chapterId, color }: { chapterId: ChapterIdInfo['id']; color?: string }) => {
const { t } = useTranslation();
const { t } = useLingui();
const download = Chapters.useDownloadStatusFromCache(chapterId);
@@ -35,6 +36,7 @@ export const DownloadStateIndicator = ({ chapterId, color }: { chapterId: Chapte
const isPartiallyDownloaded = download.progress !== 0;
const progress = `${Math.round(download.progress * 100)}%`;
const stateText = t(DOWNLOAD_STATE_TO_TRANSLATION_MAP[download.state]);
return (
<Box
@@ -61,11 +63,12 @@ export const DownloadStateIndicator = ({ chapterId, color }: { chapterId: Chapte
<Typography variant="caption" component="div" sx={{ color }}>
<>
{isDownloading && progress}
{!isDownloading &&
t('global.value', {
value: t(DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP[download.state]),
unit: isPartiallyDownloaded ? ` (${progress})` : '',
})}
{!isDownloading && (
<>
{stateText}
{isPartiallyDownloaded ? ` (${progress})` : ''}
</>
)}
</>
</Typography>
</Box>

View File

@@ -11,10 +11,10 @@
import { type JSX, useMemo, useState } from 'react';
import Typography from '@mui/material/Typography';
import { SxProps, Theme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Collapse from '@mui/material/Collapse';
import { useLingui } from '@lingui/react/macro';
import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts';
const ERROR_FACES = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
@@ -33,7 +33,7 @@ export interface EmptyViewProps {
}
const ExtraMessage = ({ messageExtra }: Pick<EmptyViewProps, 'messageExtra'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const [showFullError, setShowFullError] = useState(false);
@@ -69,7 +69,7 @@ const ExtraMessage = ({ messageExtra }: Pick<EmptyViewProps, 'messageExtra'>) =>
{graphqlError}
</Typography>
<Button variant="text" onClick={() => setShowFullError(!showFullError)} sx={{ pointerEvents: 'all' }}>
{t(showFullError ? 'global.button.show_less' : 'global.button.show_more')}
{showFullError ? t`Show less` : t`Show more`}
</Button>
</Stack>
<Collapse in={showFullError}>
@@ -86,7 +86,7 @@ const ExtraMessage = ({ messageExtra }: Pick<EmptyViewProps, 'messageExtra'>) =>
};
export function EmptyView({ message, messageExtra, retry, noFaces, sx }: EmptyViewProps) {
const { t } = useTranslation();
const { t } = useLingui();
const errorFace = useMemo(() => getRandomErrorFace(), []);
@@ -115,7 +115,7 @@ export function EmptyView({ message, messageExtra, retry, noFaces, sx }: EmptyVi
<ExtraMessage messageExtra={messageExtra} />
{retry && (
<Button onClick={retry} sx={{ pointerEvents: 'all' }}>
{t('global.button.retry')}
{t`Retry`}
</Button>
)}
</Stack>

View File

@@ -8,7 +8,7 @@
import { Component, ErrorInfo, ReactNode, useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { t } from 'i18next';
import { t } from '@lingui/core/macro';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx';
@@ -57,7 +57,7 @@ class RealErrorBoundary extends Component<Props, State> {
if (error) {
return (
<EmptyView
message={t('global.error.label.unrecoverable_error')}
message={t`Something went wrong\nAn error occurred that we cannot recover from`}
messageExtra={getErrorMessage(error)}
retry={() => window.location.reload()}
/>

View File

@@ -6,27 +6,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { closeSnackbar, CustomContentProps, SnackbarContent, VariantType } from 'notistack';
import { ForwardedRef, Fragment, memo } from 'react';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts';
import { TranslationKey } from '@/base/Base.types.ts';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
const MAX_DESCRIPTION_LENGTH = 200;
const SNACKBAR_VARIANT_TO_TRANSLATION_KEY: Record<VariantType, TranslationKey> = {
default: 'global.label.info',
info: 'global.label.info',
success: 'global.label.success',
warning: 'global.label.warning',
error: 'global.label.error',
const SNACKBAR_VARIANT_TO_TRANSLATION: Record<VariantType, MessageDescriptor> = {
default: msg`Information`,
info: msg`Information`,
success: msg`Success`,
warning: msg`Warning`,
error: msg`Error`,
};
export const SnackbarWithDescription = memo(
@@ -41,7 +42,7 @@ export const SnackbarWithDescription = memo(
description?: string;
ref?: ForwardedRef<HTMLDivElement>;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const severity = variant === 'default' ? 'info' : variant;
@@ -87,11 +88,13 @@ export const SnackbarWithDescription = memo(
title:
typeof message === 'string'
? message
: t(SNACKBAR_VARIANT_TO_TRANSLATION_KEY[variant]),
: t(SNACKBAR_VARIANT_TO_TRANSLATION[variant]),
message: description ?? '',
actions: {
cancel: { show: false },
confirm: { title: t('global.label.close') },
confirm: {
title: t`Close`,
},
},
}).catch(
defaultPromiseErrorHandler(
@@ -101,7 +104,7 @@ export const SnackbarWithDescription = memo(
}}
size="small"
>
{t('global.button.show_more')}
{t`Show more`}
</Button>
) : (
''

View File

@@ -17,8 +17,8 @@ import IconButton from '@mui/material/IconButton';
import FilterListIcon from '@mui/icons-material/FilterList';
import ListItemText from '@mui/material/ListItemText';
import ListItem from '@mui/material/ListItem';
import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
import { languageSortComparator } from '@/base/utils/Languages.ts';
@@ -31,7 +31,7 @@ interface IProps {
}
export function LanguageSelect(props: IProps) {
const { t } = useTranslation();
const { t } = useLingui();
const { selectedLanguages, setSelectedLanguages, languages } = props;
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueISOLanguageCodes(selectedLanguages));
@@ -66,13 +66,13 @@ export function LanguageSelect(props: IProps) {
return (
<>
<CustomTooltip title={t('settings.title')}>
<CustomTooltip title={t`Settings`}>
<IconButton onClick={() => setOpen(true)} aria-label="display more actions" edge="end" color="inherit">
<FilterListIcon />
</IconButton>
</CustomTooltip>
<Dialog fullWidth maxWidth="xs" open={open} onClose={handleCancel}>
<DialogTitle>{t('global.language.title.enabled_languages')}</DialogTitle>
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
<DialogContent dividers sx={{ padding: 0 }}>
<Virtuoso
style={{
@@ -97,10 +97,10 @@ export function LanguageSelect(props: IProps) {
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -12,10 +12,10 @@ import { useState } from 'react';
import IconButton from '@mui/material/IconButton';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
export const PasswordTextField = (props: TextFieldProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show);
@@ -24,7 +24,7 @@ export const PasswordTextField = (props: TextFieldProps) => {
<TextField
id="password"
name="password"
label={t('global.label.password')}
label={t`Password`}
type={showPassword ? 'text' : 'password'}
slotProps={{
input: {

View File

@@ -6,30 +6,27 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t as translate } from 'i18next';
import { TranslationKey } from '@/base/Base.types.ts';
import { MessageDescriptor } from '@lingui/core';
import { i18n } from '@/i18n';
export const createGetMenuItemTitle =
<Action extends string>(
isSingleMode: boolean,
actionToTranslationKey: Record<
actionToTranslation: Record<
Action,
{
action: {
single: TranslationKey;
selected: TranslationKey;
single: MessageDescriptor;
selected: MessageDescriptor;
};
success: TranslationKey;
error: TranslationKey;
success: MessageDescriptor;
error: MessageDescriptor;
}
>,
) =>
(action: Action, count: number): string => {
const countSuffix = count > 0 ? ` (${count})` : '';
return `${translate(
actionToTranslationKey[action].action[isSingleMode ? 'single' : 'selected'],
)}${countSuffix}`;
return `${i18n._(actionToTranslation[action].action[isSingleMode ? 'single' : 'selected'])}${countSuffix}`;
};
export const createShouldShowMenuItem =

View File

@@ -10,11 +10,11 @@ import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import { AwaitableComponentProps } from 'awaitable-component';
import { Link as RouterLink } from 'react-router-dom';
import { useLingui } from '@lingui/react/macro';
type Action = {
show?: boolean;
@@ -43,7 +43,7 @@ export const ConfirmDialog = ({
actions?: Actions;
onExtra?: () => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const actions = {
extra: {
@@ -54,12 +54,12 @@ export const ConfirmDialog = ({
},
cancel: {
show: passedActions?.cancel?.show ?? true,
title: passedActions?.cancel?.title ?? t('global.button.cancel'),
title: passedActions?.cancel?.title ?? t`Cancel`,
contain: passedActions?.cancel?.contain ?? false,
},
confirm: {
show: passedActions?.confirm?.show ?? true,
title: passedActions?.confirm?.title ?? t('global.button.ok'),
title: passedActions?.confirm?.title ?? t`Ok`,
contain:
!passedActions?.extra?.contain &&
!passedActions?.cancel?.contain &&

View File

@@ -9,13 +9,13 @@
import Dialog from '@mui/material/Dialog';
import { AwaitableComponent, AwaitableComponentProps } from 'awaitable-component';
import DialogTitle from '@mui/material/DialogTitle';
import { useTranslation } from 'react-i18next';
import DialogContent from '@mui/material/DialogContent';
import TextField from '@mui/material/TextField';
import { useState } from 'react';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
export const LoginDialog = ({
@@ -42,7 +42,7 @@ export const LoginDialog = ({
serverAddress?: string;
withServerAddress?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [serverAddress, setServerAddress] = useState(initialServerAddress);
const [username, setUsername] = useState(initialUsername);
@@ -60,7 +60,7 @@ export const LoginDialog = ({
margin="dense"
id="serverAddress"
name="serverAddress"
label={t('settings.about.server.label.address')}
label={t`Server address`}
type="text"
fullWidth
variant="standard"
@@ -73,7 +73,7 @@ export const LoginDialog = ({
margin="dense"
id="username"
name="username"
label={t('global.label.username')}
label={t`Username`}
type="text"
fullWidth
variant="standard"
@@ -89,7 +89,7 @@ export const LoginDialog = ({
)}
</DialogContent>
<DialogActions>
<Button onClick={() => onSubmit()}>{t('global.button.cancel')}</Button>
<Button onClick={() => onSubmit()}>{t`Cancel`}</Button>
<Button
variant="contained"
disabled={
@@ -101,7 +101,7 @@ export const LoginDialog = ({
}
onClick={() => loginLogout(username, password, serverAddress)}
>
{t(isLoggedIn ? 'global.button.log_out' : 'global.button.log_in')}
{isLoggedIn ? t`Log out` : t`Log in`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -12,10 +12,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 Stack from '@mui/material/Stack';
import { useCallback, useMemo } from 'react';
import { CheckboxProps } from '@mui/material/Checkbox';
import { useLingui } from '@lingui/react/macro';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts';
@@ -42,7 +42,7 @@ export function CheckboxListSetting<Item>({
checkbox?: Omit<CheckboxProps, 'checked' | 'onChange' | 'label'>;
};
}) {
const { t } = useTranslation();
const { t } = useLingui();
const itemIds = useMemo(() => items.map(getId), [items]);
const currentSelectedItemIds = useMemo(() => items.filter(isChecked).map(getId), [items]);
@@ -105,15 +105,15 @@ export function CheckboxListSetting<Item>({
}}
>
<Button onClick={() => handleSelectAll(!selectedItemIds.length, items.map(getId))}>
{t(selectedItemIds.length ? 'global.button.reset' : 'global.button.select_all')}
{selectedItemIds.length ? t`Reset` : t`Select all`}
</Button>
<Stack direction="row">
<Button autoFocus onClick={handleCancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
{!!items.length && (
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
)}
</Stack>

View File

@@ -15,11 +15,11 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { useLingui } from '@lingui/react/macro';
export const DateSetting = ({
settingName,
@@ -34,7 +34,7 @@ export const DateSetting = ({
handleChange: (path?: string | null) => void;
remove?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value ?? defaultValue);
@@ -85,7 +85,6 @@ export const DateSetting = ({
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{settingName}</DialogTitle>
<DialogContent>
@@ -117,7 +116,7 @@ export const DateSetting = ({
}}
color="primary"
>
{t('global.button.reset_to_default')}
{t`Reset to Default`}
</Button>
)}
{remove && (
@@ -128,13 +127,13 @@ export const DateSetting = ({
}}
color="primary"
>
{t('global.button.remove')}
{t`Remove`}
</Button>
)}
</Stack>
<Stack direction="row">
<Button onClick={closeDialogWithReset} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button
onClick={() => {
@@ -142,7 +141,7 @@ export const DateSetting = ({
}}
color="primary"
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</Stack>
</Stack>

View File

@@ -17,12 +17,12 @@ import Typography from '@mui/material/Typography';
import { useEffect, useState, type JSX } from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { TextSetting, TextSettingProps } from '@/base/components/settings/text/TextSetting.tsx';
import { TextSettingDialog } from '@/base/components/settings/text/TextSettingDialog.tsx';
@@ -38,7 +38,7 @@ const MutableListItem = ({
mutable?: boolean;
deletable?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', alignItems: 'center' }}>
@@ -49,7 +49,7 @@ const MutableListItem = ({
<ListItemText secondary={textSettingProps.value} />
</ListItem>
)}
<CustomTooltip title={t('chapter.action.download.delete.label.action')} disabled={!deletable}>
<CustomTooltip title={t`Delete`} disabled={!deletable}>
<IconButton disabled={!deletable} onClick={handleDelete}>
<DeleteIcon />
</IconButton>
@@ -87,7 +87,7 @@ export const MutableListSetting = ({
validateItem = () => true,
invalidItemError,
}: MutableListSettingProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const values = getValues(valueInfos);
@@ -129,7 +129,7 @@ export const MutableListSetting = ({
}
if (!validateItem?.(newValue, dialogValues)) {
makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error');
makeToast(invalidItemError ?? t`Invalid input`, 'error');
return;
}
@@ -156,7 +156,6 @@ export const MutableListSetting = ({
}}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogTitle>{settingName}</DialogTitle>
{(!!description || !!dialogDisclaimer) && (
@@ -219,17 +218,14 @@ export const MutableListSetting = ({
width: '100%',
}}
>
<Button onClick={() => setIsAddItemDialogOpen(true)}>
{addItemButtonTitle ?? t('global.button.add')}
</Button>
<Button onClick={() => setIsAddItemDialogOpen(true)}>{addItemButtonTitle ?? t`Add`}</Button>
<Stack direction="row">
<Button onClick={() => closeDialog()}>{t('global.button.cancel')}</Button>
<Button onClick={() => saveChanges()}>{t('global.button.ok')}</Button>
<Button onClick={() => closeDialog()}>{t`Cancel`}</Button>
<Button onClick={() => saveChanges()}>{t`Ok`}</Button>
</Stack>
</Stack>
</DialogActions>
</Dialog>
{isAddItemDialogOpen && (
<TextSettingDialog
settingName=""

View File

@@ -17,7 +17,6 @@ import Typography from '@mui/material/Typography';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ListItemButton from '@mui/material/ListItemButton';
import * as React from 'react';
import ListItemIcon from '@mui/material/ListItemIcon';
@@ -25,6 +24,7 @@ import Slider from '@mui/material/Slider';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { SxProps, Theme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
type BaseProps = {
settingTitle: string;
@@ -70,7 +70,7 @@ export const NumberSetting = ({
handleLiveUpdate,
listItemTextSx: sx,
}: Props) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value);
@@ -125,7 +125,6 @@ export const NumberSetting = ({
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={cancel}>
<DialogTitle>{dialogTitle}</DialogTitle>
<DialogContent>
@@ -172,7 +171,7 @@ export const NumberSetting = ({
value={dialogValue}
type="number"
error={isInvalid}
helperText={isInvalid ? t('global.error.label.invalid_input') : ''}
helperText={isInvalid ? t`Invalid input` : ''}
onChange={(e) => {
const newValue = Number(e.target.value);
updateValue(newValue, false);
@@ -201,14 +200,14 @@ export const NumberSetting = ({
<DialogActions>
{defaultValue !== undefined ? (
<Button onClick={resetToDefault} color="primary">
{t('global.button.reset_to_default')}
{t`Reset to Default`}
</Button>
) : null}
<Button onClick={cancel} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button disabled={isInvalid} onClick={submit} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -18,17 +19,15 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DialogContentText from '@mui/material/DialogContentText';
import InfoIcon from '@mui/icons-material/Info';
import { useLingui } from '@lingui/react/macro';
import { Select } from '@/base/components/inputs/Select.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
export type SelectSettingValueDisplayInfo = {
text: TranslationKey | string;
description?: TranslationKey | string;
disclaimer?: TranslationKey | string;
text: MessageDescriptor | string;
description?: MessageDescriptor | string;
disclaimer?: MessageDescriptor | string;
};
export type SelectSettingValue<Value> = [Value: Value, DisplayInfo: SelectSettingValueDisplayInfo];
@@ -48,7 +47,7 @@ export const SelectSetting = <SettingValue extends string | number>({
handleChange: (value: SettingValue) => void;
disabled?: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value);
@@ -82,11 +81,10 @@ export const SelectSetting = <SettingValue extends string | number>({
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={settingName}
secondary={valueDisplayText ? t(valueDisplayText as TranslationKey) : t('global.label.loading')}
secondary={valueDisplayText ? t(valueDisplayText as MessageDescriptor) : t`Loading`}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
<DialogTitle>{settingName}</DialogTitle>
<DialogContent>
@@ -102,7 +100,7 @@ export const SelectSetting = <SettingValue extends string | number>({
whiteSpace: 'pre-line',
}}
>
{t(dialogValueDisplayInfo.description as TranslationKey)}
{t(dialogValueDisplayInfo.description as MessageDescriptor)}
</Typography>
)}
{dialogValueDisplayInfo.disclaimer && (
@@ -121,7 +119,7 @@ export const SelectSetting = <SettingValue extends string | number>({
whiteSpace: 'pre-line',
}}
>
{t(dialogValueDisplayInfo.disclaimer as TranslationKey)}
{t(dialogValueDisplayInfo.disclaimer as MessageDescriptor)}
</Typography>
</Stack>
)}
@@ -135,7 +133,7 @@ export const SelectSetting = <SettingValue extends string | number>({
>
{values.map(([selectValue, { text: selectText }]) => (
<MenuItem key={selectValue} value={selectValue}>
{t(selectText as TranslationKey)}
{t(selectText as MessageDescriptor)}
</MenuItem>
))}
</Select>
@@ -143,10 +141,10 @@ export const SelectSetting = <SettingValue extends string | number>({
</DialogContent>
<DialogActions>
<Button onClick={() => closeDialog()} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={() => updateSetting()} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -14,11 +14,11 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';
import { useLingui } from '@lingui/react/macro';
export const TimeSetting = ({
settingName,
@@ -31,7 +31,7 @@ export const TimeSetting = ({
defaultValue: string;
handleChange: (path: string) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value);
@@ -82,7 +82,6 @@ export const TimeSetting = ({
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{settingName}</DialogTitle>
<DialogContent>
@@ -105,11 +104,11 @@ export const TimeSetting = ({
}}
color="primary"
>
{t('global.button.reset_to_default')}
{t`Reset to Default`}
</Button>
) : null}
<Button onClick={closeDialogWithReset} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button
onClick={() => {
@@ -117,7 +116,7 @@ export const TimeSetting = ({
}}
color="primary"
>
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -14,7 +14,7 @@ import DialogContentText from '@mui/material/DialogContentText';
import TextField from '@mui/material/TextField';
import DialogActions from '@mui/material/DialogActions';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
export type TextSettingDialogProps = {
@@ -42,7 +42,7 @@ export const TextSettingDialog = ({
setIsDialogOpen,
validate = () => true,
}: TextSettingDialogProps) => {
const { t } = useTranslation();
const { t } = useLingui();
const [dialogValue, setDialogValue] = useState(value ?? '');
const [isValidValue, setIsValidValue] = useState(true);
@@ -89,7 +89,7 @@ export const TextSettingDialog = ({
placeholder={placeholder}
value={dialogValue}
error={error}
helperText={error ? t('global.error.label.invalid_input') : ''}
helperText={error ? t`Invalid input` : ''}
onChange={(e) => {
const newValue = e.target.value;
@@ -100,10 +100,10 @@ export const TextSettingDialog = ({
</DialogContent>
<DialogActions>
<Button onClick={() => closeDialog()} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={() => updateSetting()} disabled={!isValidValue} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -8,28 +8,20 @@
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Trans, useTranslation } from 'react-i18next';
import { TranslationKey } from '@/base/Base.types.ts';
interface SuperscriptProps {
superscript: string;
text: string;
}
/**
* Expects a translation key of format "{{value}}<0>superscript</0>"
* @param i18nKey
* @param value
* @constructor
* Displays a text with a superscript portion.
*/
export const Superscript = ({ i18nKey, value }: { i18nKey: TranslationKey; value: string }) => {
const { t } = useTranslation();
return (
<Stack sx={{ flexDirection: 'row', gap: 0.25 }}>
<Trans
t={t}
// the type of "key" causes tsc error: "TS2590: Expression produces a union type that is too complex to represent"
i18nKey={i18nKey as any}
values={{ value }}
components={[<Typography variant="caption" sx={{ fontSize: 'x-small', opacity: 0.75 }} />]}
/>
</Stack>
);
};
export const Superscript = ({ superscript, text }: SuperscriptProps) => (
<Stack sx={{ flexDirection: 'row', gap: 0.25 }}>
{text}
<Typography variant="caption" sx={{ fontSize: 'x-small', opacity: 0.75 }}>
{superscript}
</Typography>
</Stack>
);

View File

@@ -12,6 +12,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { SnackbarProvider } from 'notistack';
import { I18nProvider } from '@lingui/react';
import { ActiveDeviceContextProvider } from '@/features/device/DeviceContext.tsx';
import { AppHotkeysProvider } from '@/features/hotkeys/AppHotkeysProvider.tsx';
import { SnackbarWithDescription } from '@/base/components/feedback/SnackbarWithDescription.tsx';
@@ -19,6 +20,7 @@ import { AppPageHistoryContextProvider } from '@/base/contexts/AppPageHistoryCon
import { AppThemeContextProvider } from '@/features/theme/AppThemeContext.tsx';
import { NavBarContextProvider } from '@/features/navigation-bar/NavbarContext.tsx';
import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts';
import { i18n } from '@/i18n';
interface Props {
children: React.ReactNode;
@@ -27,27 +29,29 @@ interface Props {
export const AppContext: React.FC<Props> = ({ children }) => (
<Router basename={SubpathUtil.getRouterBasename()}>
<StyledEngineProvider injectFirst>
<AppThemeContextProvider>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<NavBarContextProvider>
<AppPageHistoryContextProvider>
<ActiveDeviceContextProvider>
<SnackbarProvider
Components={{
default: SnackbarWithDescription,
info: SnackbarWithDescription,
success: SnackbarWithDescription,
warning: SnackbarWithDescription,
error: SnackbarWithDescription,
}}
>
<AppHotkeysProvider>{children}</AppHotkeysProvider>
</SnackbarProvider>
</ActiveDeviceContextProvider>
</AppPageHistoryContextProvider>
</NavBarContextProvider>
</QueryParamProvider>
</AppThemeContextProvider>
<I18nProvider i18n={i18n}>
<AppThemeContextProvider>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<NavBarContextProvider>
<AppPageHistoryContextProvider>
<ActiveDeviceContextProvider>
<SnackbarProvider
Components={{
default: SnackbarWithDescription,
info: SnackbarWithDescription,
success: SnackbarWithDescription,
warning: SnackbarWithDescription,
error: SnackbarWithDescription,
}}
>
<AppHotkeysProvider>{children}</AppHotkeysProvider>
</SnackbarProvider>
</ActiveDeviceContextProvider>
</AppPageHistoryContextProvider>
</NavBarContextProvider>
</QueryParamProvider>
</AppThemeContextProvider>
</I18nProvider>
</StyledEngineProvider>
</Router>
);

View File

@@ -7,7 +7,7 @@
*/
import dayjs, { Dayjs } from 'dayjs';
import { t } from 'i18next';
import { t } from '@lingui/core/macro';
export const timeFormatter = new Intl.DateTimeFormat(navigator.language, { hour: '2-digit', minute: '2-digit' });
export const dateFormatter = new Intl.DateTimeFormat(navigator.language, {
@@ -53,18 +53,18 @@ export const getDateString = (date: Dayjs | number, withTime: boolean = false) =
if (actualDate.isToday()) {
if (withTime) {
return t('global.date.label.today_at', { timeString });
return t`Today at ${timeString}`;
}
return t('global.date.label.today');
return t`Today`;
}
if (actualDate.isYesterday()) {
if (withTime) {
return t('global.date.label.yesterday_at', { timeString });
return t`Yesterday at ${timeString}`;
}
return t('global.date.label.yesterday');
return t`Yesterday`;
}
return dateFormatter.format(actualDate.toDate());

View File

@@ -6,11 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t } from 'i18next';
import { TranslationKey } from '@/base/Base.types.ts';
import { MessageDescriptor } from '@lingui/core';
import { msg, t } from '@lingui/core/macro';
import { getISOLanguage, getPreferredISOLanguageCodes, LanguageObject } from '@/lib/ISOLanguageUtil.ts';
import { i18n } from '@/i18n';
export enum DefaultLanguage {
ALL = 'all',
OTHER = 'other',
@@ -19,12 +21,12 @@ export enum DefaultLanguage {
LAST_USED_SOURCE = 'last_used_source',
}
const DEFAULT_LANGUAGE_TO_TRANSLATION: Record<DefaultLanguage, TranslationKey> = {
[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',
const DEFAULT_LANGUAGE_TO_TRANSLATION: Record<DefaultLanguage, MessageDescriptor> = {
[DefaultLanguage.ALL]: msg`All`,
[DefaultLanguage.OTHER]: msg`Other`,
[DefaultLanguage.LOCAL_SOURCE]: msg`Other`,
[DefaultLanguage.PINNED]: msg`Pinned`,
[DefaultLanguage.LAST_USED_SOURCE]: msg`Last used`,
};
export function getLanguage(code: string): LanguageObject {
@@ -37,15 +39,15 @@ export function getLanguage(code: string): LanguageObject {
return {
orgCode: code,
isoCode: code,
name: t('global.language.label.language_with_code', { code }),
nativeName: t('global.language.label.language_with_code', { code }),
name: t`Language with code: ${code}`,
nativeName: t`Language with code: ${code}`,
};
}
export function languageCodeToName(code: string): string {
const isCustomLanguage = Object.keys(DEFAULT_LANGUAGE_TO_TRANSLATION).includes(code);
if (isCustomLanguage) {
return t(DEFAULT_LANGUAGE_TO_TRANSLATION[code as DefaultLanguage]);
return i18n._(DEFAULT_LANGUAGE_TO_TRANSLATION[code as DefaultLanguage]);
}
return getLanguage(code).nativeName;

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>

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