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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user