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,22 +6,23 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { BackupFlag, BackupFlagGroup } from '@/features/backup/Backup.types.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, TranslationKey> = {
|
||||
includeManga: 'settings.backup.flag.manga',
|
||||
includeCategories: 'settings.backup.flag.categories',
|
||||
includeChapters: 'settings.backup.flag.chapters',
|
||||
includeClientData: 'settings.backup.flag.client_data',
|
||||
includeHistory: 'settings.backup.flag.history',
|
||||
includeServerSettings: 'settings.backup.flag.server_settings',
|
||||
includeTracking: 'settings.backup.flag.tracking',
|
||||
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, MessageDescriptor> = {
|
||||
includeManga: msg`Library entries`,
|
||||
includeCategories: msg`Categories`,
|
||||
includeChapters: msg`Chapters`,
|
||||
includeClientData: msg`Client data`,
|
||||
includeHistory: msg`History`,
|
||||
includeServerSettings: msg`Server settings`,
|
||||
includeTracking: msg`Tracking`,
|
||||
};
|
||||
|
||||
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, TranslationKey> = {
|
||||
[BackupFlagGroup.LIBRARY]: 'settings.backup.flag.group.library',
|
||||
[BackupFlagGroup.SETTINGS]: 'settings.backup.flag.group.settings',
|
||||
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, MessageDescriptor> = {
|
||||
[BackupFlagGroup.LIBRARY]: msg`Library`,
|
||||
[BackupFlagGroup.SETTINGS]: msg`Settings`,
|
||||
};
|
||||
|
||||
export const BACKUP_FLAGS = Object.keys(BACKUP_FLAGS_TO_TRANSLATION) as readonly BackupFlag[];
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { t as translate } from 'i18next';
|
||||
import { plural, t } from '@lingui/core/macro';
|
||||
import { AutoBackupFlagInclusionState, BackupFlag, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
|
||||
import { BACKUP_FLAGS_TO_TRANSLATION } from '@/features/backup/Backup.constants.ts';
|
||||
|
||||
@@ -32,11 +32,11 @@ export const convertToBackupFlags = (flags: AutoBackupFlagInclusionState): Backu
|
||||
|
||||
const getIncludeExcludeText = (count: number, allCount: number, specificText: string): string => {
|
||||
if (count === 0) {
|
||||
return translate('global.label.none');
|
||||
return t`None`;
|
||||
}
|
||||
|
||||
if (count === allCount) {
|
||||
return translate('extension.language.all');
|
||||
return t`All`;
|
||||
}
|
||||
|
||||
return specificText;
|
||||
@@ -49,9 +49,9 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
|
||||
const flagsByState = Object.groupBy(Object.entries(flags), ([, value]) => value.toString());
|
||||
|
||||
const includedFlagsString =
|
||||
flagsByState.true?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
flagsByState.true?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
const excludedFlagsString =
|
||||
flagsByState.false?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
flagsByState.false?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
|
||||
return {
|
||||
false: getIncludeExcludeText(flagsByState.false?.length ?? 0, totalFlags, excludedFlagsString),
|
||||
@@ -61,8 +61,11 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
|
||||
|
||||
export const getBackupCleanupDisplayValue = (ttl: number): string => {
|
||||
if (ttl === 0) {
|
||||
return translate('global.label.never');
|
||||
return t`Never`;
|
||||
}
|
||||
|
||||
return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl });
|
||||
return plural(ttl, {
|
||||
one: `Delete backups that are older than # day`,
|
||||
other: `Delete backups that are older than # days`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,12 +11,12 @@ import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { AwaitableComponentProps } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import {
|
||||
BACKUP_FLAG_GROUP_TO_TRANSLATION,
|
||||
@@ -34,7 +34,7 @@ export const BackupFlagInclusionDialog = ({
|
||||
title,
|
||||
flags,
|
||||
}: AwaitableComponentProps<BackupFlagInclusionState> & { title: string; flags?: BackupFlagInclusionState }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [includeStateByFlag, setIncludeStateByFlag] = useState(
|
||||
Object.fromEntries(BACKUP_FLAGS.map((flag) => [flag, flags?.[flag] ?? true])) as BackupFlagInclusionState,
|
||||
@@ -67,10 +67,10 @@ export const BackupFlagInclusionDialog = ({
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={onDismiss} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button onClick={() => onSubmit(includeStateByFlag)} color="primary">
|
||||
{t('global.button.ok')}
|
||||
{t`Ok`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import List from '@mui/material/List';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
@@ -17,6 +16,7 @@ import ListItem from '@mui/material/ListItem';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { AwaitableComponentProps } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { BrowseTab } from '@/features/browse/Browse.types.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { ValidateBackupResult } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -28,16 +28,16 @@ export const BackupValidationDialog = ({
|
||||
isVisible,
|
||||
onExitComplete,
|
||||
}: AwaitableComponentProps & { validationResult: ValidateBackupResult }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Dialog open={isVisible} onTransitionExited={onExitComplete} onClose={onDismiss}>
|
||||
<DialogTitle>{t('settings.backup.action.validate.dialog.title')}</DialogTitle>
|
||||
<DialogTitle>{t`Backup validation`}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{!!validationResult?.missingSources.length && (
|
||||
<List
|
||||
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
|
||||
subheader={t('settings.backup.action.validate.dialog.content.label.missing_sources')}
|
||||
subheader={t`The following sources are not installed:`}
|
||||
>
|
||||
{validationResult?.missingSources.map(({ id, name }) => (
|
||||
<ListItem sx={{ display: 'list-item' }} key={id}>
|
||||
@@ -49,7 +49,7 @@ export const BackupValidationDialog = ({
|
||||
{!!validationResult?.missingTrackers.length && (
|
||||
<List
|
||||
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
|
||||
subheader={t('settings.backup.action.validate.dialog.content.label.missing_trackers')}
|
||||
subheader={t`The following trackers are not logged in:`}
|
||||
>
|
||||
{validationResult?.missingTrackers.map(({ name }) => (
|
||||
<ListItem sx={{ display: 'list-item' }} key={name}>
|
||||
@@ -75,7 +75,7 @@ export const BackupValidationDialog = ({
|
||||
autoFocus={!!validationResult?.missingSources.length}
|
||||
variant={validationResult?.missingSources.length ? 'contained' : 'text'}
|
||||
>
|
||||
{t('extension.action.label.install')}
|
||||
{t`Install`}
|
||||
</Button>
|
||||
)}
|
||||
{!!validationResult?.missingTrackers.length && (
|
||||
@@ -86,11 +86,11 @@ export const BackupValidationDialog = ({
|
||||
autoFocus={!!validationResult?.missingTrackers.length}
|
||||
variant={validationResult?.missingTrackers.length ? 'contained' : 'text'}
|
||||
>
|
||||
{t('global.button.log_in')}
|
||||
{t`Log in`}
|
||||
</Button>
|
||||
)}
|
||||
<Stack direction="row">
|
||||
<Button onClick={onDismiss}>{t('global.button.cancel')}</Button>
|
||||
<Button onClick={onDismiss}>{t`Cancel`}</Button>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
autoFocus={
|
||||
@@ -102,7 +102,7 @@ export const BackupValidationDialog = ({
|
||||
: 'text'
|
||||
}
|
||||
>
|
||||
{t('global.button.restore')}
|
||||
{t`Restore`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -10,12 +10,13 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { fromEvent } from 'file-selector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { useEventListener, useMergedRef, useWindowEvent } from '@mantine/hooks';
|
||||
import { AwaitableComponent } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -41,9 +42,9 @@ import { BackupSettingsType } from '@/features/backup/Backup.types.ts';
|
||||
let backupRestoreId: string | undefined;
|
||||
|
||||
export function Backup() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('settings.backup.title'));
|
||||
useAppTitle(t`Backup`);
|
||||
|
||||
const {
|
||||
data: settingsData,
|
||||
@@ -76,7 +77,7 @@ export function Backup() {
|
||||
value: BackupSettingsType[Setting],
|
||||
) => {
|
||||
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,7 +85,7 @@ export function Backup() {
|
||||
settings: Record<Setting, BackupSettingsType[Setting]>,
|
||||
) => {
|
||||
mutateSettings({ variables: { input: { settings } } }).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,11 +100,11 @@ export function Backup() {
|
||||
const isRestoreFinished = isSuccess || isFailure;
|
||||
if (isRestoreFinished) {
|
||||
if (isSuccess) {
|
||||
makeToast(t('settings.backup.action.restore.label.success'), 'success');
|
||||
makeToast(t`Backup restored.`, 'success');
|
||||
}
|
||||
|
||||
if (isFailure) {
|
||||
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error');
|
||||
makeToast(t`Could not restore backup`, 'error');
|
||||
}
|
||||
|
||||
requestManager.reset();
|
||||
@@ -121,21 +122,17 @@ export function Backup() {
|
||||
|
||||
const createBackup = async () => {
|
||||
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
|
||||
title: t('settings.backup.action.create.label.title'),
|
||||
title: t`Create backup`,
|
||||
});
|
||||
|
||||
makeToast(t('settings.backup.action.create.label.in_progress'), 'info');
|
||||
makeToast(t`Creating backup…`, 'info');
|
||||
|
||||
try {
|
||||
const backupFileResponse = await requestManager.createBackupFile({ flags }).response;
|
||||
|
||||
const backupFileUrl = backupFileResponse.data?.createBackup.url;
|
||||
if (!backupFileUrl) {
|
||||
makeToast(
|
||||
t('settings.backup.action.create.error.failure'),
|
||||
'error',
|
||||
getErrorMessage(backupFileResponse.errors),
|
||||
);
|
||||
makeToast(t`Could not create backup`, 'error', getErrorMessage(backupFileResponse.errors));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +143,7 @@ export function Backup() {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.action.create.error.failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not create backup`, 'error', getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,7 +169,7 @@ export function Backup() {
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.action.validate.error.label.failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not validate backup`, 'error', getErrorMessage(e));
|
||||
} finally {
|
||||
resetBackupState();
|
||||
}
|
||||
@@ -182,17 +179,17 @@ export function Backup() {
|
||||
|
||||
const restoreBackup = async (backup: File) => {
|
||||
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
|
||||
title: t('settings.backup.action.restore.label.title'),
|
||||
title: t`Restore Backup`,
|
||||
});
|
||||
|
||||
try {
|
||||
makeToast(t('settings.backup.action.restore.label.in_progress'), 'info');
|
||||
makeToast(t`Restoring backup…`, 'info');
|
||||
|
||||
const response = await requestManager.restoreBackupFile({ backup, flags }).response;
|
||||
backupRestoreId = response.data?.restoreBackup.id;
|
||||
setTriggerReRender(Date.now());
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not restore backup`, 'error', getErrorMessage(e));
|
||||
} finally {
|
||||
resetBackupState();
|
||||
}
|
||||
@@ -200,13 +197,13 @@ export function Backup() {
|
||||
|
||||
const submitBackup = async (file: File) => {
|
||||
if (file.name.toLowerCase().endsWith('json')) {
|
||||
makeToast(t('settings.backup.action.restore.error.label.legacy_backup_unsupported'), 'error');
|
||||
makeToast(t`legacy backups are not supported!`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const isValidFilename = file.name.toLowerCase().match(/proto\.gz$|tachibk$/g);
|
||||
if (!isValidFilename) {
|
||||
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
||||
makeToast(t`Invalid filetype`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,7 +235,7 @@ export function Backup() {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('Backup::refetch'))}
|
||||
/>
|
||||
@@ -247,19 +244,20 @@ export function Backup() {
|
||||
|
||||
const backupSettings = settingsData!.settings;
|
||||
|
||||
const autoBackupFlagsInfo = getAutoBackupFlagsInfo(backupSettings);
|
||||
const includedCategoriesText = autoBackupFlagsInfo.true;
|
||||
const excludedCategoriesText = autoBackupFlagsInfo.false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<List sx={{ padding: 0 }}>
|
||||
<ListItemButton onClick={createBackup}>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.action.create.label.title')}
|
||||
secondary={t('settings.backup.action.create.label.description')}
|
||||
/>
|
||||
<ListItemText primary={t`Create backup`} secondary={t`Back up library as a Tachiyomi backup`} />
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => inputRef.current?.click()} disabled={!!backupRestoreId}>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.action.restore.label.title')}
|
||||
secondary={t('settings.backup.action.restore.label.description')}
|
||||
primary={t`Restore Backup`}
|
||||
secondary={t`You can also drag and drop the backup file here to restore it`}
|
||||
/>
|
||||
{backupRestoreId ? (
|
||||
<ListItemIcon>
|
||||
@@ -275,19 +273,17 @@ export function Backup() {
|
||||
}
|
||||
>
|
||||
<TextSetting
|
||||
settingName={t('settings.backup.automated.location.label.title')}
|
||||
dialogDescription={t('settings.backup.automated.location.label.description')}
|
||||
settingName={t`Backup location`}
|
||||
dialogDescription={t`The path to the directory on the server where automated backups should get saved in`}
|
||||
value={backupSettings.backupPath}
|
||||
settingDescription={
|
||||
backupSettings.backupPath.length ? backupSettings.backupPath : t('global.label.default')
|
||||
}
|
||||
settingDescription={backupSettings.backupPath.length ? backupSettings.backupPath : t`Default`}
|
||||
handleChange={(path) => updateSetting('backupPath', path)}
|
||||
/>
|
||||
<ListItemButton
|
||||
onClick={async () => {
|
||||
try {
|
||||
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
|
||||
title: t('settings.backup.automated.flags.title'),
|
||||
title: t`Backup data`,
|
||||
flags: convertToBackupFlags(backupSettings),
|
||||
});
|
||||
|
||||
@@ -298,54 +294,46 @@ export function Backup() {
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.automated.flags.title')}
|
||||
primary={t`Backup data`}
|
||||
secondary={
|
||||
<>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.include', {
|
||||
includedCategoriesText: getAutoBackupFlagsInfo(backupSettings).true,
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.exclude', {
|
||||
excludedCategoriesText: getAutoBackupFlagsInfo(backupSettings).false,
|
||||
})}
|
||||
</span>
|
||||
<span>{t`Include: ${includedCategoriesText}`}</span>
|
||||
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
|
||||
</>
|
||||
}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<TimeSetting
|
||||
settingName={t('settings.backup.automated.label.time')}
|
||||
settingName={t`Backup time`}
|
||||
value={backupSettings.backupTime}
|
||||
defaultValue="00:00"
|
||||
handleChange={(time: string) => updateSetting('backupTime', time)}
|
||||
/>
|
||||
<NumberSetting
|
||||
settingTitle={t('settings.backup.automated.label.interval')}
|
||||
settingValue={t('global.date.value.label.day', {
|
||||
days: backupSettings.backupInterval,
|
||||
count: backupSettings.backupInterval,
|
||||
settingTitle={t`Backup interval`}
|
||||
settingValue={plural(backupSettings.backupInterval, {
|
||||
one: '# day',
|
||||
other: '# days',
|
||||
})}
|
||||
value={backupSettings.backupInterval}
|
||||
defaultValue={1}
|
||||
minValue={1}
|
||||
maxValue={31}
|
||||
stepSize={1}
|
||||
valueUnit={t('global.date.label.day_one')}
|
||||
valueUnit={t`Day`}
|
||||
showSlider
|
||||
handleUpdate={(interval: number) => updateSetting('backupInterval', interval)}
|
||||
/>
|
||||
<NumberSetting
|
||||
settingTitle={t('settings.backup.automated.cleanup.label.title')}
|
||||
settingTitle={t`Backup cleanup`}
|
||||
settingValue={getBackupCleanupDisplayValue(backupSettings.backupTTL)}
|
||||
value={backupSettings.backupTTL}
|
||||
defaultValue={14}
|
||||
minValue={0}
|
||||
maxValue={1000}
|
||||
stepSize={1}
|
||||
valueUnit={t('global.date.label.day_one')}
|
||||
valueUnit={t`Day`}
|
||||
showSlider
|
||||
handleUpdate={(ttl: number) => updateSetting('backupTTL', ttl)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user