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;