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,6 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { d } from 'koration';
import { DEFAULT_DEVICE } from '@/features/device/services/Device.ts';
import { DEFAULT_SORT_SETTINGS } from '@/features/migration/Migration.constants.ts';
@@ -16,7 +18,7 @@ import {
MetadataServerSettings,
ServerSettings,
} from '@/features/settings/Settings.types.ts';
import { GridLayout, TranslationKey } from '@/base/Base.types.ts';
import { GridLayout } from '@/base/Base.types';
import { getDefaultLanguages } from '@/base/utils/Languages.ts';
import { SelectSettingValue, SelectSettingValueDisplayInfo } from '@/base/components/settings/SelectSetting.tsx';
import {
@@ -93,93 +95,93 @@ export const SERVER_SETTINGS_METADATA_DEFAULT: MetadataServerSettings = {
};
const AUTH_MODES = [AuthMode.None].concat(Object.values(AuthMode).filter((mode) => mode !== AuthMode.None));
const AUTH_MODES_TO_TRANSLATION_KEY: { [mode in AuthMode]: SelectSettingValueDisplayInfo } = {
const AUTH_MODES_TO_TRANSLATION: { [mode in AuthMode]: SelectSettingValueDisplayInfo } = {
[AuthMode.None]: {
text: 'settings.server.auth.mode.option.none.label.title',
description: 'settings.server.auth.mode.option.none.label.description',
disclaimer: 'settings.server.auth.mode.option.none.label.info',
text: msg`None`,
description: msg`Disable authentication`,
disclaimer: msg`Your library will be accessible. Use this only on private networks or when otherwise securing access.`,
},
[AuthMode.BasicAuth]: {
text: 'settings.server.auth.mode.option.basicAuth.label.title',
description: 'settings.server.auth.mode.option.basicAuth.label.description',
text: msg`Basic Authentication`,
description: msg`Your browser will prompt you to enter credentials with a dialog.`,
},
[AuthMode.SimpleLogin]: {
text: 'settings.server.auth.mode.option.simple_login.label.title',
description: 'settings.server.auth.mode.option.simple_login.label.description',
disclaimer: 'settings.server.auth.mode.option.simple_login.label.info',
text: msg`Simple Login`,
description: msg`The login will be handled by the server.`,
disclaimer: msg`When you enable this, you may need to refresh this tab for the login page to appear.`,
},
[AuthMode.UiLogin]: {
text: 'settings.server.auth.mode.option.ui_login.label.title',
description: 'settings.server.auth.mode.option.ui_login.label.description',
text: msg`UI Login`,
description: msg`The login will be handled by the client.`,
},
};
export const AUTH_MODES_SELECT_VALUES: SelectSettingValue<AuthMode>[] = AUTH_MODES.map((mode) => [
mode,
AUTH_MODES_TO_TRANSLATION_KEY[mode],
AUTH_MODES_TO_TRANSLATION[mode],
]);
const WEB_UI_FLAVORS = Object.values(WebUiFlavor);
const WEB_UI_FLAVOR_TO_TRANSLATION_KEY: { [flavor in WebUiFlavor]: SelectSettingValueDisplayInfo } = {
const WEB_UI_FLAVOR_TO_TRANSLATION: { [flavor in WebUiFlavor]: SelectSettingValueDisplayInfo } = {
[WebUiFlavor.Webui]: {
text: 'settings.webui.title.webui',
description: 'settings.webui.flavor.option.webui.label.description',
disclaimer: 'settings.webui.flavor.label.info',
text: msg`WebUI`,
description: msg`Use the default WebUI`,
disclaimer: msg`After changing this setting go to the "About" page and check for a webUI update and install it`,
},
[WebUiFlavor.Vui]: {
text: 'settings.webui.flavor.option.vui.label.title',
description: 'settings.webui.flavor.option.vui.label.description',
disclaimer: 'settings.webui.flavor.label.info',
text: msg`VUI`,
description: msg`A preview focused web frontend built with svelte`,
disclaimer: msg`After changing this setting go to the "About" page and check for a webUI update and install it`,
},
[WebUiFlavor.Custom]: {
text: 'settings.webui.flavor.option.custom.label.title',
description: 'settings.webui.flavor.option.custom.label.description',
text: msg`Custom`,
description: msg`Use a custom WebUI.\nTo use a custom WebUI replace the content of the "webUI" directory in the root directory on the server with the files of the custom WebUI`,
},
};
export const WEB_UI_FLAVOR_SELECT_VALUES: SelectSettingValue<WebUiFlavor>[] = WEB_UI_FLAVORS.map((flavor) => [
flavor,
WEB_UI_FLAVOR_TO_TRANSLATION_KEY[flavor],
WEB_UI_FLAVOR_TO_TRANSLATION[flavor],
]);
const WEB_UI_CHANNELS = Object.values(WebUiChannel);
const WEB_UI_CHANNEL_TO_TRANSLATION_KEYS: {
const WEB_UI_CHANNEL_TO_TRANSLATIONS: {
[channel in WebUiChannel]: SelectSettingValueDisplayInfo;
} = {
[WebUiChannel.Bundled]: {
text: 'settings.webui.channel.option.bundled.label.title',
description: 'settings.webui.channel.option.bundled.label.description',
disclaimer: 'settings.webui.flavor.label.info',
text: msg`Bundled`,
description: msg`Use the version that was delivered with the server release`,
disclaimer: msg`After changing this setting go to the "About" page and check for a webUI update and install it`,
},
[WebUiChannel.Stable]: {
text: 'settings.webui.channel.option.stable.label.title',
description: 'settings.webui.channel.option.stable.label.description',
disclaimer: 'settings.webui.flavor.label.info',
text: msg`Stable`,
description: msg`Use the latest released version.`,
disclaimer: msg`After changing this setting go to the "About" page and check for a webUI update and install it`,
},
[WebUiChannel.Preview]: {
text: 'settings.webui.channel.option.preview.label.title',
description: 'settings.webui.channel.option.preview.label.description',
disclaimer: 'settings.webui.channel.option.preview.label.disclaimer',
text: msg`Preview`,
description: msg`Use the latest features and help us get them ready for a stable release.`,
disclaimer: msg`Features and changes in this version might not be completely ready yet and can cause bugs.\nMake sure that you have automatic backups enabled to prevent loss of your library!\n\nAfter changing this setting go to the "About" page and check for a webUI update and install it`,
},
};
export const WEB_UI_CHANNEL_SELECT_VALUES: SelectSettingValue<WebUiChannel>[] = WEB_UI_CHANNELS.map((channel) => [
channel,
WEB_UI_CHANNEL_TO_TRANSLATION_KEYS[channel],
WEB_UI_CHANNEL_TO_TRANSLATIONS[channel],
]);
const WEB_UI_INTERFACES = Object.values(WebUiInterface);
const WEB_UI_INTERFACE_TO_TRANSLATION_KEYS: {
const WEB_UI_INTERFACE_TO_TRANSLATIONS: {
[webUIInterface in WebUiInterface]: SelectSettingValueDisplayInfo;
} = {
[WebUiInterface.Browser]: {
text: 'settings.webui.interface.option.label.browser',
description: 'settings.webui.interface.label.description',
text: msg`Browser`,
description: msg`Where to start the WebUI when starting the server`,
},
[WebUiInterface.Electron]: {
text: 'settings.webui.interface.option.label.electron',
description: 'settings.webui.interface.label.description',
text: msg`Electron`,
description: msg`Where to start the WebUI when starting the server`,
},
};
export const WEB_UI_INTERFACE_SELECT_VALUES: SelectSettingValue<WebUiInterface>[] = WEB_UI_INTERFACES.map(
(webUIInterface) => [webUIInterface, WEB_UI_INTERFACE_TO_TRANSLATION_KEYS[webUIInterface]],
(webUIInterface) => [webUIInterface, WEB_UI_INTERFACE_TO_TRANSLATIONS[webUIInterface]],
);
export const GLOBAL_UPDATE_INTERVAL = {
@@ -189,11 +191,11 @@ export const GLOBAL_UPDATE_INTERVAL = {
};
export const GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION: {
[setting in keyof GlobalUpdateSkipEntriesSettings]: TranslationKey;
[setting in keyof GlobalUpdateSkipEntriesSettings]: MessageDescriptor;
} = {
excludeUnreadChapters: 'library.settings.global_update.entries.label.unread_chapters',
excludeNotStarted: 'library.settings.global_update.entries.label.not_started',
excludeCompleted: 'library.settings.global_update.entries.label.completed',
excludeUnreadChapters: msg`With unread chapter(s)`,
excludeNotStarted: msg`That haven't been started`,
excludeCompleted: msg`With "Completed" status`,
};
export const WEB_UI_UPDATE_INTERVAL = {
@@ -222,46 +224,43 @@ export const KOREADER_SYNC_PERCENTAGE_TOLERANCE = {
};
export const KOREADER_SYNC_CONFLICT_STRATEGIES = Object.values(KoreaderSyncConflictStrategy);
export const KOREADER_SYNC_CONFLICT_STRATEGY_TO_TRANSLATION_KEYS: Record<
export const KOREADER_SYNC_CONFLICT_STRATEGY_TO_TRANSLATIONS: Record<
KoreaderSyncConflictStrategy,
SelectSettingValueDisplayInfo
> = {
[KoreaderSyncConflictStrategy.Disabled]: {
text: 'settings.server.koreader.sync.strategy.option.disabled',
text: msg`Disabled`,
},
[KoreaderSyncConflictStrategy.KeepLocal]: {
text: 'settings.server.koreader.sync.strategy.option.keep_local',
text: msg`Keep local`,
},
[KoreaderSyncConflictStrategy.KeepRemote]: {
text: 'settings.server.koreader.sync.strategy.option.keep_remote',
text: msg`Keep remote`,
},
[KoreaderSyncConflictStrategy.Prompt]: {
text: 'settings.server.koreader.sync.strategy.option.prompt',
text: msg`Prompt`,
},
};
export const KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES: SelectSettingValue<KoreaderSyncConflictStrategy>[] =
KOREADER_SYNC_CONFLICT_STRATEGIES.map((strategy) => [
strategy,
KOREADER_SYNC_CONFLICT_STRATEGY_TO_TRANSLATION_KEYS[strategy],
KOREADER_SYNC_CONFLICT_STRATEGY_TO_TRANSLATIONS[strategy],
]);
export const KOREADER_SYNC_CHECKSUM_METHODES = Object.values(KoreaderSyncChecksumMethod);
export const KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATION_KEYS: Record<
export const KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATIONS: Record<
KoreaderSyncChecksumMethod,
SelectSettingValueDisplayInfo
> = {
[KoreaderSyncChecksumMethod.Binary]: {
text: 'settings.server.koreader.sync.check_sum_method.binary',
text: msg`Binary`,
},
[KoreaderSyncChecksumMethod.Filename]: {
text: 'settings.server.koreader.sync.check_sum_method.filename',
text: msg`Filename`,
},
};
export const KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES: SelectSettingValue<KoreaderSyncChecksumMethod>[] =
KOREADER_SYNC_CHECKSUM_METHODES.map((method) => [
method,
KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATION_KEYS[method],
]);
KOREADER_SYNC_CHECKSUM_METHODES.map((method) => [method, KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATIONS[method]]);
export const IMAGE_PROCESSING_COMPRESSION = {
min: 0,
@@ -280,32 +279,32 @@ export const IMAGE_PROCESSING_CONNECT_TIMEOUT = {
};
const IMAGE_PROCESSING_TARGET_MODES = Object.values(ImageProcessingTargetMode);
const IMAGE_PROCESSING_TARGET_MODES_TO_TRANSLATION_KEY: {
const IMAGE_PROCESSING_TARGET_MODES_TO_TRANSLATION: {
[flavor in ImageProcessingTargetMode]: SelectSettingValueDisplayInfo;
} = {
[ImageProcessingTargetMode.DISABLED]: {
text: 'global.label.disabled',
text: msg`Disabled`,
},
[ImageProcessingTargetMode.IMAGE]: {
text: 'download.settings.conversion.target_modes.image.title',
description: 'download.settings.conversion.target_modes.image.description',
text: msg`Image`,
description: msg`Convert images to different formats`,
},
[ImageProcessingTargetMode.URL]: {
text: 'download.settings.conversion.target_modes.url.title',
description: 'download.settings.conversion.target_modes.image.description',
text: msg`URL`,
description: msg`Convert images to different formats`,
},
};
export const IMAGE_PROCESSING_TARGET_MODES_SELECT_VALUES: SelectSettingValue<ImageProcessingTargetMode>[] =
IMAGE_PROCESSING_TARGET_MODES.map((mode) => [mode, IMAGE_PROCESSING_TARGET_MODES_TO_TRANSLATION_KEY[mode]]);
IMAGE_PROCESSING_TARGET_MODES.map((mode) => [mode, IMAGE_PROCESSING_TARGET_MODES_TO_TRANSLATION[mode]]);
export const IMAGE_PROCESSING_INPUT_WIDTH = 250;
export const DEFAULT_MIME_TYPE = 'default';
export const MIME_TYPE_PREFIX = 'image/';
export const TARGET_DISABLED = 'none';
export const IMAGE_PROCESSING_TYPE_TO_TRANSLATION: Record<ImageProcessingType, TranslationKey> = {
[ImageProcessingType.DOWNLOAD]: 'download.settings.conversion.title',
[ImageProcessingType.SERVE]: 'settings.images.processing.serve.title',
export const IMAGE_PROCESSING_TYPE_TO_TRANSLATION: Record<ImageProcessingType, MessageDescriptor> = {
[ImageProcessingType.DOWNLOAD]: msg`Image download processing`,
[ImageProcessingType.SERVE]: msg`Image serve processing`,
};
export const IMAGE_PROCESSING_TYPE_TO_SETTING: Record<

View File

@@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useLingui } from '@lingui/react/macro';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
export const ServerAddressSetting = () => {
const { t } = useTranslation();
const { t } = useLingui();
const [serverAddress, setServerAddress] = requestManager.useBaseUrl();
@@ -23,7 +23,7 @@ export const ServerAddressSetting = () => {
return (
<TextSetting
settingName={t('settings.about.server.label.address')}
settingName={t`Server address`}
handleChange={handleServerAddressChange}
value={serverAddress}
placeholder="http://localhost:4567"

View File

@@ -8,10 +8,10 @@
import List from '@mui/material/List';
import ListSubheader from '@mui/material/ListSubheader';
import { useTranslation } from 'react-i18next';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { useLingui } from '@lingui/react/macro';
import {
CategoriesInclusionSetting,
CategoriesInclusionSettingProps,
@@ -30,7 +30,7 @@ export const GlobalUpdateSettings = ({
serverSettings: ServerSettings;
categories: CategoriesInclusionSettingProps['categories'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const { updateMangas } = serverSettings;
const [mutateSettings] = requestManager.useUpdateServerSettings();
@@ -42,7 +42,7 @@ export const GlobalUpdateSettings = ({
try {
await 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));
}
};
@@ -50,7 +50,7 @@ export const GlobalUpdateSettings = ({
<List
subheader={
<ListSubheader component="div" id="global-update-settings">
{t('library.settings.global_update.title')}
{t`Global update`}
</ListSubheader>
}
>
@@ -59,12 +59,12 @@ export const GlobalUpdateSettings = ({
<CategoriesInclusionSetting
categories={categories}
includeField="includeInUpdate"
dialogText={t('library.settings.global_update.categories.label.info')}
dialogText={t`Entries in excluded categories will not be updated even if they are also in included categories`}
/>
<ListItem>
<ListItemText
primary={t('library.settings.global_update.metadata.label.title')}
secondary={t('library.settings.global_update.metadata.label.description')}
primary={t`Automatically refresh metadata`}
secondary={t`Check for new cover and details when updating library`}
/>
<Switch
edge="end"

View File

@@ -6,8 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t as translate } from 'i18next';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
@@ -16,6 +14,8 @@ 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 { useLingui } from '@lingui/react/macro';
import { t as translate } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { CheckboxContainer } from '@/base/components/inputs/CheckboxContainer.ts';
@@ -28,20 +28,20 @@ const getSkipMangasText = (settings: GlobalUpdateSkipEntriesSettings) => {
const skipSettings: string[] = [];
if (settings.excludeUnreadChapters) {
skipSettings.push(translate(GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION.excludeUnreadChapters) as string);
skipSettings.push(translate(GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION.excludeUnreadChapters));
}
if (settings.excludeNotStarted) {
skipSettings.push(translate(GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION.excludeNotStarted) as string);
skipSettings.push(translate(GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION.excludeNotStarted));
}
if (settings.excludeCompleted) {
skipSettings.push(translate(GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION.excludeCompleted) as string);
skipSettings.push(translate(GLOBAL_UPDATE_SKIP_ENTRIES_TO_TRANSLATION.excludeCompleted));
}
const isNothingExcluded = !skipSettings.length;
if (isNothingExcluded) {
skipSettings.push(translate('global.label.none'));
skipSettings.push(translate`None`);
}
return skipSettings.join(', ');
@@ -54,7 +54,7 @@ const extractSkipEntriesSettings = (serverSettings: ServerSettings): GlobalUpdat
});
export const GlobalUpdateSettingsEntries = ({ serverSettings }: { serverSettings: ServerSettings }) => {
const { t } = useTranslation();
const { t } = useLingui();
const globalUpdateSettings = extractSkipEntriesSettings(serverSettings);
const [mutateSettings] = requestManager.useUpdateServerSettings();
@@ -81,7 +81,7 @@ export const GlobalUpdateSettingsEntries = ({ serverSettings }: { serverSettings
try {
await mutateSettings({ variables: { input: { settings: dialogSettings } } });
} catch (e) {
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e));
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e));
}
};
@@ -106,14 +106,13 @@ export const GlobalUpdateSettingsEntries = ({ serverSettings }: { serverSettings
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('library.settings.global_update.entries.title')}
primary={t`Skip updating entries`}
secondary={skipEntriesText}
onClick={() => setIsDialogOpen(true)}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogTitle>{t('library.settings.global_update.entries.title')}</DialogTitle>
<DialogTitle>{t`Skip updating entries`}</DialogTitle>
<DialogContent>
<CheckboxContainer>
{Object.entries(dialogSettings).map(([setting, value]) => (
@@ -137,10 +136,10 @@ export const GlobalUpdateSettingsEntries = ({ serverSettings }: { serverSettings
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
{t('global.button.cancel')}
{t`Cancel`}
</Button>
<Button onClick={updateSettings} color="primary">
{t('global.button.ok')}
{t`Ok`}
</Button>
</DialogActions>
</Dialog>

View File

@@ -6,17 +6,17 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { useCallback } from 'react';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/base/hooks/usePersistedValue.tsx';
import { ServerSettings } from '@/features/settings/Settings.types.ts';
import { GLOBAL_UPDATE_INTERVAL } from '@/features/settings/Settings.constants.ts';
export const GlobalUpdateSettingsInterval = ({
@@ -24,7 +24,7 @@ export const GlobalUpdateSettingsInterval = ({
}: {
globalUpdateInterval: ServerSettings['globalUpdateInterval'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const autoUpdateIntervalHours = globalUpdateInterval;
const doAutoUpdates = !!autoUpdateIntervalHours;
@@ -54,20 +54,18 @@ export const GlobalUpdateSettingsInterval = ({
return (
<List>
<ListItem>
<ListItemText primary={t('library.settings.global_update.auto_update.label.title')} />
<ListItemText primary={t`Automatic updates`} />
<Switch edge="end" checked={doAutoUpdates} onChange={(e) => setDoAutoUpdates(e.target.checked)} />
</ListItem>
<NumberSetting
settingTitle={t('library.settings.global_update.auto_update.interval.label.title')}
settingValue={t('library.settings.global_update.auto_update.interval.label.value', {
hours: currentAutoUpdateIntervalHours,
})}
settingTitle={t`Automatic update interval`}
settingValue={t`${currentAutoUpdateIntervalHours}h`}
value={currentAutoUpdateIntervalHours}
minValue={GLOBAL_UPDATE_INTERVAL.min}
maxValue={GLOBAL_UPDATE_INTERVAL.max}
defaultValue={GLOBAL_UPDATE_INTERVAL.default}
showSlider
valueUnit={t('global.time.hour_short')}
valueUnit={t`h`}
handleUpdate={updateSetting}
disabled={!doAutoUpdates}
/>

View File

@@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { IMAGE_PROCESSING_INPUT_WIDTH } from '@/features/settings/Settings.constants.ts';
import { TSettingsDownloadConversionKeyValueItem } from '@/features/settings/Settings.types';
@@ -26,7 +26,7 @@ export const KeyValueItem = ({
isDuplicate: boolean;
onChange: (header: TSettingsDownloadConversionKeyValueItem | null) => void;
} & TSettingsDownloadConversionKeyValueItem) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
return (
@@ -51,10 +51,10 @@ export const KeyValueItem = ({
<TextField
autoFocus
sx={{ width: IMAGE_PROCESSING_INPUT_WIDTH }}
label={t('download.settings.conversion.headers.name')}
label={t`Name`}
value={name}
error={isDuplicate}
helperText={isDuplicate && t('global.error.label.invalid_input')}
helperText={isDuplicate && t`Invalid input`}
onChange={(e) =>
onChange({
id,
@@ -65,7 +65,7 @@ export const KeyValueItem = ({
/>
<TextField
sx={{ width: IMAGE_PROCESSING_INPUT_WIDTH }}
label={t('download.settings.conversion.headers.value')}
label={t`Value`}
value={value}
onChange={(e) =>
onChange({
@@ -76,7 +76,7 @@ export const KeyValueItem = ({
}
/>
</Stack>
<CustomTooltip disabled={false} title={t('chapter.action.download.delete.label.action')}>
<CustomTooltip disabled={false} title={t`Delete`}>
<IconButton
onClick={() => {
onChange(null);

View File

@@ -6,11 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Collapse from '@mui/material/Collapse';
import { useLingui } from '@lingui/react/macro';
import { Maybe } from '@/lib/graphql/generated/graphql.ts';
import { addStableIdToKeyValueItems, isDuplicateKeyValueItem } from '@/features/settings/ImageProcessing.utils.ts';
import { KeyValueItem } from '@/features/settings/components/images/KeyValueItem.tsx';
@@ -27,7 +27,7 @@ export const KeyValueItems = ({
items: Maybe<TSettingsDownloadConversionKeyValueItem[] | undefined>;
onChange: (items: Maybe<TSettingsDownloadConversionKeyValueItem[]>) => void;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
return (
<Collapse in={open}>
@@ -56,7 +56,7 @@ export const KeyValueItems = ({
variant="contained"
sx={{ width: 'fit-content' }}
>
{t('global.button.add')}
{t`Add`}
</Button>
</Stack>
</Collapse>

View File

@@ -6,9 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import { useLingui } from '@lingui/react/macro';
import { ImageProcessingTargetMode } from '@/features/settings/Settings.types.ts';
import { IMAGE_PROCESSING_INPUT_WIDTH, MIME_TYPE_PREFIX } from '@/features/settings/Settings.constants.ts';
import { isUrlTargetMode } from '@/features/settings/ImageProcessing.utils.ts';
@@ -30,7 +30,7 @@ export const MimeTypeTextField = ({
onUpdate: (value: string) => void;
mode: ImageProcessingTargetMode;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const isImageMode = mode === ImageProcessingTargetMode.IMAGE;
const isValidUrl = !value.length || isUrlTargetMode(value);
@@ -44,7 +44,7 @@ export const MimeTypeTextField = ({
value={value}
disabled={isDefault}
error={!isValid}
helperText={!isValid && t('global.error.label.invalid_input')}
helperText={!isValid && t`Invalid input`}
slotProps={{
input: {
startAdornment: (

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
@@ -20,9 +19,10 @@ import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import { d } from 'koration';
import { useElementSize } from '@mantine/hooks';
import { useLingui } from '@lingui/react/macro';
import { MessageDescriptor } from '@lingui/core';
import { KeyValueItems } from '@/features/settings/components/images/KeyValueItems.tsx';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
import {
IMAGE_PROCESSING_CALL_TIMEOUT,
IMAGE_PROCESSING_COMPRESSION,
@@ -55,7 +55,7 @@ export const Processing = ({
onChange: (newConversion: TSettingsDownloadConversion | null) => void;
isDuplicate: boolean;
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const theme = useTheme();
const { ref: textFieldRef, height: textFieldHeight } = useElementSize();
@@ -101,7 +101,7 @@ export const Processing = ({
shouldAutoFocus
isDefault={isDefault}
isDuplicate={isDuplicate}
label={t('download.settings.conversion.mime_type')}
label={t`MIME-Type`}
value={mimeType}
onUpdate={(value) => {
onChange({
@@ -118,14 +118,12 @@ export const Processing = ({
</TypographyMaxLines>
<FormControl sx={{ minWidth: '100px' }}>
<InputLabel id="image-conversion-target-mode-label">
{t('download.settings.conversion.target_modes.title')}
</InputLabel>
<InputLabel id="image-conversion-target-mode-label">{t`Target mode`}</InputLabel>
<Select
ref={textFieldRef}
id="image-conversion-target-mode"
labelId="image-conversion-target-mode-label"
label={t('download.settings.conversion.target_modes.title')}
label={t`Target mode`}
value={mode}
onChange={(e) => {
if (!isUrlMode) {
@@ -145,7 +143,7 @@ export const Processing = ({
>
{IMAGE_PROCESSING_TARGET_MODES_SELECT_VALUES.map(([selectValue, { text: selectText }]) => (
<MenuItem key={selectValue} value={selectValue}>
{t(selectText as TranslationKey)}
{t(selectText as MessageDescriptor)}
</MenuItem>
))}
</Select>
@@ -156,7 +154,7 @@ export const Processing = ({
shouldAutoFocus={false}
isDefault={false}
isDuplicate={false}
label={t('download.settings.conversion.target')}
label={t`MIME-Type target`}
value={target}
onUpdate={(value) => {
onChange({
@@ -176,11 +174,11 @@ export const Processing = ({
return (
<TextField
sx={{ width: IMAGE_PROCESSING_INPUT_WIDTH }}
label={t('download.settings.conversion.compression_level')}
label={t`Compression level`}
value={compressionLevel ?? ''}
type="number"
error={!isCompressionLevelValid}
helperText={!isCompressionLevelValid ? t('global.error.label.invalid_input') : ''}
helperText={!isCompressionLevelValid ? t`Invalid input` : ''}
slotProps={{
input: {
inputProps: IMAGE_PROCESSING_COMPRESSION,
@@ -200,19 +198,15 @@ export const Processing = ({
<>
<TextField
sx={{ width: IMAGE_PROCESSING_INPUT_WIDTH }}
label={t('download.settings.conversion.call_timeout')}
label={t`Call timeout`}
value={callTimeout ? d(callTimeout).seconds.inWholeSeconds : ''}
type="number"
error={!isCallTimeoutValid}
helperText={!isCallTimeoutValid ? t('global.error.label.invalid_input') : ''}
helperText={!isCallTimeoutValid ? t`Invalid input` : ''}
slotProps={{
input: {
inputProps: IMAGE_PROCESSING_CALL_TIMEOUT,
endAdornment: (
<InputAdornment position="end">
{t('global.date.label.second_other')}
</InputAdornment>
),
endAdornment: <InputAdornment position="end">{t`Second`}</InputAdornment>,
},
}}
onChange={(e) => {
@@ -226,19 +220,15 @@ export const Processing = ({
/>
<TextField
sx={{ width: IMAGE_PROCESSING_INPUT_WIDTH }}
label={t('download.settings.conversion.connect_timeout')}
label={t`Connect timeout`}
value={connectTimeout ? d(connectTimeout).seconds.inWholeSeconds : ''}
type="number"
error={!isConnectTimeoutValid}
helperText={!isConnectTimeoutValid ? t('global.error.label.invalid_input') : ''}
helperText={!isConnectTimeoutValid ? t`Invalid input` : ''}
slotProps={{
input: {
inputProps: IMAGE_PROCESSING_CONNECT_TIMEOUT,
endAdornment: (
<InputAdornment position="end">
{t('global.date.label.second_other')}
</InputAdornment>
),
endAdornment: <InputAdornment position="end">{t`Second`}</InputAdornment>,
},
}}
onChange={(e) => {
@@ -256,24 +246,20 @@ export const Processing = ({
variant={areSearchParamsCollapsed ? 'outlined' : 'contained'}
sx={{ height: textFieldHeight }}
>
{t('download.settings.conversion.search_params.button', {
count: searchParams?.length ?? 0,
})}
{t`Search parameters (${searchParams?.length ?? 0})`}
</Button>
<Button
onClick={() => setAreHeadersCollapsed(!areHeadersCollapsed)}
variant={areHeadersCollapsed ? 'outlined' : 'contained'}
sx={{ height: textFieldHeight }}
>
{t('download.settings.conversion.headers.button', {
count: headers?.length ?? 0,
})}
{t`Headers (${headers?.length ?? 0})`}
</Button>
</>
);
})()}
</Stack>
<CustomTooltip disabled={isDisabled} title={t('chapter.action.download.delete.label.action')}>
<CustomTooltip disabled={isDisabled} title={t`Delete`}>
<IconButton
disabled={isDisabled}
onClick={() => {
@@ -285,7 +271,7 @@ export const Processing = ({
</CustomTooltip>
</Stack>
<KeyValueItems
title={t('download.settings.conversion.search_params.title')}
title={t`Search parameters`}
open={isUrlMode && !areSearchParamsCollapsed}
items={searchParams}
onChange={(params) => {
@@ -300,7 +286,7 @@ export const Processing = ({
}}
/>
<KeyValueItems
title={t('download.settings.conversion.headers.title')}
title={t`Headers`}
open={isUrlMode && !areHeadersCollapsed}
items={headers}
onChange={(updatedHeaders) =>

View File

@@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListSubheader from '@mui/material/ListSubheader';
import ListItemText from '@mui/material/ListItemText';
import ListItemButton from '@mui/material/ListItemButton';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { SelectSetting } from '@/base/components/settings/SelectSetting.tsx';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
@@ -51,15 +51,13 @@ export const KoreaderSyncSettings = ({
onCompletion?: (success: boolean) => void,
) => Promise<void>;
} & Omit<KoSyncStatusPayload, '__typename'>) => {
const { t } = useTranslation();
const { t } = useLingui();
const handleLogout = async () => {
const { data } = await requestManager.koSyncLogout().response;
if (data?.logoutKoSyncAccount.status.isLoggedIn) {
throw new Error(
t('tracking.action.logout.label.failure', { name: t('settings.server.koreader.sync.title') }),
);
throw new Error(t`Could not log out from KOReader Sync`);
}
};
@@ -67,7 +65,7 @@ export const KoreaderSyncSettings = ({
const { data } = await requestManager.koSyncLogin(serverAddress, username, password).response;
if (!data?.connectKoSyncAccount.status.isLoggedIn) {
throw new Error(data?.connectKoSyncAccount.message ?? t('global.label.unknown'));
throw new Error(data?.connectKoSyncAccount.message ?? t`Unknown`);
}
};
@@ -78,20 +76,8 @@ export const KoreaderSyncSettings = ({
) => {
const controlled = CredentialsLogin.showControlled(
{
title: t(
isLoggedIn
? 'settings.server.koreader.sync.connection.disconnect'
: 'settings.server.koreader.sync.connection.connect',
{
name: t('settings.server.koreader.sync.title'),
},
),
description: isLoggedIn
? t('settings.server.koreader.sync.connection.connected', {
username: initialUsername,
serverAddress: initialServerAddress,
})
: undefined,
title: isLoggedIn ? t`Disconnect from KOReader Sync server` : t`Connect to KOReader Sync server`,
description: isLoggedIn ? t`Connected as ${initialUsername} to ${initialServerAddress}` : undefined,
isLoading: false,
isLoggedIn,
withServerAddress: true,
@@ -107,11 +93,7 @@ export const KoreaderSyncSettings = ({
controlled.submit();
} catch (e) {
makeToast(
t('settings.server.koreader.sync.connection.message.disconnect_error'),
'error',
getErrorMessage(e),
);
makeToast(t`Failed to disconnect from KOReader Sync server.`, 'error', getErrorMessage(e));
}
return;
@@ -123,11 +105,7 @@ export const KoreaderSyncSettings = ({
controlled.submit();
} catch (e) {
makeToast(
t('settings.server.koreader.sync.connection.message.connect_error'),
'error',
getErrorMessage(e),
);
makeToast(t`Failed to connect to KOReader Sync server.`, 'error', getErrorMessage(e));
const RETRY_KEY = '__retry__';
const retry = await Promise.race([controlled.promise, Promise.resolve(RETRY_KEY)]);
@@ -145,7 +123,7 @@ export const KoreaderSyncSettings = ({
<List
subheader={
<ListSubheader component="div" id="koreader-sync-settings">
{t('settings.server.koreader.sync.title')}
{t`KOReader Sync`}
</ListSubheader>
}
>
@@ -153,42 +131,33 @@ export const KoreaderSyncSettings = ({
onClick={() => handleLoginLogout(currentServerAddress ?? undefined, currentUsername ?? undefined)}
>
<ListItemText
primary={t('settings.server.koreader.sync.connection.status')}
primary={t`Sync status`}
secondary={
isLoggedIn
? t('settings.server.koreader.sync.connection.connected', {
username: currentUsername,
serverAddress: currentServerAddress,
})
: t('settings.server.koreader.sync.connection.disconnected')
isLoggedIn ? t`Connected as ${currentUsername} to ${currentServerAddress}` : t`Disconnected`
}
/>
</ListItemButton>
<SelectSetting<KoreaderSyncConflictStrategy>
settingName={t('settings.server.koreader.sync.strategy.forward_title')}
settingName={t`Sync to a newer state`}
value={koreaderSyncStrategyForward}
values={KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES}
handleChange={(value) => updateSetting('koreaderSyncStrategyForward', value)}
/>
<SelectSetting<KoreaderSyncConflictStrategy>
settingName={t('settings.server.koreader.sync.strategy.backward_title')}
settingName={t`Sync to an older state`}
value={koreaderSyncStrategyBackward}
values={KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES}
handleChange={(value) => updateSetting('koreaderSyncStrategyBackward', value)}
/>
<SelectSetting<KoreaderSyncChecksumMethod>
settingName={t('settings.server.koreader.sync.check_sum_method.title')}
settingName={t`Document matching method`}
value={koreaderSyncChecksumMethod}
values={KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES}
handleChange={(value) => updateSetting('koreaderSyncChecksumMethod', value)}
/>
<NumberSetting
settingTitle={t('settings.server.koreader.sync.tolerance.title')}
dialogDescription={t('settings.server.koreader.sync.tolerance.description')}
settingTitle={t`Percentage Tolerance`}
dialogDescription={t`Sync will not be triggered if the progress difference is within this tolerance.`}
settingValue={koreaderSyncPercentageTolerance.toString()}
value={koreaderSyncPercentageTolerance}
defaultValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.default}

View File

@@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { useCallback } from 'react';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { getPersistedServerSetting, usePersistedValue } from '@/base/hooks/usePersistedValue.tsx';
@@ -27,7 +27,7 @@ export const WebUIUpdateIntervalSetting = ({
disabled?: boolean;
updateCheckInterval: ServerSettings['webUIUpdateCheckInterval'];
}) => {
const { t } = useTranslation();
const { t } = useLingui();
const shouldAutoUpdate = !!updateCheckInterval;
const [mutateSettings] = requestManager.useUpdateServerSettings();
@@ -44,7 +44,7 @@ export const WebUIUpdateIntervalSetting = ({
webUIUpdateCheckInterval === 0 ? currentUpdateCheckInterval : webUIUpdateCheckInterval,
);
mutateSettings({ variables: { input: { settings: { webUIUpdateCheckInterval } } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
},
[currentUpdateCheckInterval],
@@ -58,7 +58,7 @@ export const WebUIUpdateIntervalSetting = ({
return (
<List>
<ListItem>
<ListItemText primary={t('settings.webui.auto_update.label.title')} />
<ListItemText primary={t`Automatically download the latest version`} />
<Switch
disabled={disabled}
edge="end"
@@ -67,16 +67,14 @@ export const WebUIUpdateIntervalSetting = ({
/>
</ListItem>
<NumberSetting
settingTitle={t('settings.webui.auto_update.label.interval')}
settingValue={t('library.settings.global_update.auto_update.interval.label.value', {
hours: currentUpdateCheckInterval,
})}
settingTitle={t`Update interval`}
settingValue={`${currentUpdateCheckInterval}${t`h`}`}
value={currentUpdateCheckInterval}
minValue={WEB_UI_UPDATE_INTERVAL.min}
maxValue={WEB_UI_UPDATE_INTERVAL.max}
defaultValue={WEB_UI_UPDATE_INTERVAL.default}
showSlider
valueUnit={t('global.time.hour_short')}
valueUnit={t`h`}
handleUpdate={updateSetting}
disabled={disabled || !shouldAutoUpdate}
/>

View File

@@ -9,9 +9,9 @@
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import { useTranslation } from 'react-i18next';
import ListSubheader from '@mui/material/ListSubheader';
import Divider from '@mui/material/Divider';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -24,9 +24,9 @@ import { epochToDate } from '@/base/utils/DateHelper.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export function About() {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.about.title'));
useAppTitle(t`About`);
const { data, loading, error, refetch } = requestManager.useGetAbout({ notifyOnNetworkStatusChange: true });
@@ -57,7 +57,7 @@ export function About() {
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('About::refetch'))}
/>
@@ -78,19 +78,16 @@ export function About() {
sx={{ padding: 0 }}
subheader={
<ListSubheader component="div" id="about-server-info">
{t('settings.server.title.server')}
{t`Server`}
</ListSubheader>
}
>
<ListItem>
<ListItemText
primary={t('settings.server.title.server')}
secondary={`${aboutServer.name} (${aboutServer.buildType})`}
/>
<ListItemText primary={t`Server`} secondary={`${aboutServer.name} (${aboutServer.buildType})`} />
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.about.server.label.version')}
primary={t`Server version`}
secondary={
<VersionInfo
version={aboutServer.version}
@@ -106,7 +103,7 @@ export function About() {
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.about.server.label.build_time')}
primary={t`Build time`}
secondary={epochToDate(Number(aboutServer.buildTime)).toString()}
/>
</ListItem>
@@ -116,19 +113,16 @@ export function About() {
sx={{ padding: 0 }}
subheader={
<ListSubheader component="div" id="about-webui-info">
{t('settings.webui.title.webui')}
{t`WebUI`}
</ListSubheader>
}
>
<ListItem>
<ListItemText
primary={t('settings.about.webui.label.channel')}
secondary={aboutWebUI.channel.toLocaleUpperCase()}
/>
<ListItemText primary={t`WebUI channel`} secondary={aboutWebUI.channel.toLocaleUpperCase()} />
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.about.webui.label.version')}
primary={t`WebUI version`}
secondary={
<VersionInfo
version={aboutWebUI.tag}
@@ -152,21 +146,18 @@ export function About() {
<List
subheader={
<ListSubheader component="div" id="about-links">
{t('global.label.links')}
{t`Links`}
</ListSubheader>
}
>
<ListItemLink to={aboutServer.github} target="_blank" rel="noreferrer">
<ListItemText primary={t('settings.about.server.label.github')} secondary={aboutServer.github} />
<ListItemText primary={t`GitHub Server`} secondary={aboutServer.github} />
</ListItemLink>
<ListItemLink to="https://github.com/Suwayomi/Suwayomi-WebUI" target="_blank" rel="noreferrer">
<ListItemText
primary={t('settings.about.webui.label.github')}
secondary="https://github.com/Suwayomi/Suwayomi-WebUI"
/>
<ListItemText primary={t`GitHub WebUI`} secondary="https://github.com/Suwayomi/Suwayomi-WebUI" />
</ListItemLink>
<ListItemLink to={aboutServer.discord} target="_blank" rel="noreferrer">
<ListItemText primary={t('global.label.discord')} secondary={aboutServer.discord} />
<ListItemText primary={t`Discord`} secondary={aboutServer.discord} />
</ListItemLink>
</List>
</List>

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
@@ -15,11 +14,12 @@ import ListSubheader from '@mui/material/ListSubheader';
import Switch from '@mui/material/Switch';
import Link from '@mui/material/Link';
import { useColorScheme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { useAppThemeContext } from '@/features/theme/AppThemeContext.tsx';
import { Select } from '@/base/components/inputs/Select.tsx';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { I18nResourceCode, i18nResources } from '@/i18n';
import { I18nResourceCode, i18nResources, loadCatalog } from '@/i18n';
import { languageCodeToName } from '@/base/utils/Languages.ts';
import { ThemeList } from '@/features/theme/components/ThemeList.tsx';
import {
@@ -38,19 +38,19 @@ import { MANGA_GRID_WIDTH, SERVER_SETTINGS_METADATA_DEFAULT } from '@/features/s
import { MUI_THEME_MODE_KEY } from '@/lib/mui/MUI.constants.ts';
export const Appearance = () => {
const { t, i18n } = useTranslation();
const { t, i18n } = useLingui();
const { themeMode, setThemeMode, shouldUsePureBlackMode, setShouldUsePureBlackMode } = useAppThemeContext();
const { mode, setMode } = useColorScheme();
const actualThemeMode = (mode ?? themeMode) as ThemeMode;
useAppTitle(t('settings.appearance.title'));
useAppTitle(t`Appearance`);
const {
settings: { mangaThumbnailBackdrop, mangaDynamicColorSchemes, mangaGridItemWidth },
request: { loading, error, refetch },
} = useMetadataServerSettings();
const updateMetadataSetting = createUpdateMetadataServerSettings<keyof MetadataThemeSettings>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
);
const isDarkMode = MediaQuery.getThemeMode(actualThemeMode) === ThemeMode.DARK;
@@ -62,7 +62,7 @@ export const Appearance = () => {
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('Appearance::refetch'))}
/>
@@ -73,12 +73,12 @@ export const Appearance = () => {
<List
subheader={
<ListSubheader component="div" id="appearance-theme">
{t('settings.appearance.theme.title')}
{t`Theme`}
</ListSubheader>
}
>
<ListItem>
<ListItemText primary={t('settings.appearance.theme.mode')} />
<ListItemText primary={t`Theme mode`} />
<Select<ThemeMode>
value={actualThemeMode}
onChange={(e) => {
@@ -91,20 +91,20 @@ export const Appearance = () => {
}}
>
<MenuItem key={ThemeMode.SYSTEM} value={ThemeMode.SYSTEM}>
{t('global.label.system')}
{t`System`}
</MenuItem>
<MenuItem key={ThemeMode.DARK} value={ThemeMode.DARK}>
{t('global.label.dark')}
{t`Dark`}
</MenuItem>
<MenuItem key={ThemeMode.LIGHT} value={ThemeMode.LIGHT}>
{t('global.label.light')}
{t`Light`}
</MenuItem>
</Select>
</ListItem>
<ThemeList />
{isDarkMode && (
<ListItem>
<ListItemText primary={t('settings.appearance.theme.pure_black_mode')} />
<ListItemText primary={t`Pure black dark mode`} />
<Switch
checked={shouldUsePureBlackMode}
onChange={(_, enabled) => setShouldUsePureBlackMode(enabled)}
@@ -114,33 +114,31 @@ export const Appearance = () => {
<List
subheader={
<ListSubheader component="div" id="appearance-theme">
{t('global.label.display')}
{t`Display`}
</ListSubheader>
}
>
<ListItem>
<ListItemText
primary={t('global.language.label.language')}
primary={t`Language`}
secondary={
<>
<span>{t('settings.label.language_description')} </span>
<span>{t`Feel free to translate the project on`} </span>
<Link
href="https://hosted.weblate.org/projects/suwayomi/suwayomi-webui"
target="_blank"
rel="noreferrer"
>
{t('global.language.title.weblate')}
{t`Weblate`}
</Link>
</>
}
/>
<Select
value={i18nResources.includes(i18n.language as I18nResourceCode) ? i18n.language : 'en'}
value={i18nResources.includes(i18n.locale as I18nResourceCode) ? i18n.locale : 'en'}
onChange={({ target: { value: language } }) =>
i18n.changeLanguage(language, (e) => {
if (e) {
makeToast(t('global.language.error.load'), 'error', getErrorMessage(e));
}
loadCatalog(language).catch((e: Error) => {
makeToast(t`Could not load language`, 'error', getErrorMessage(e));
})
}
>
@@ -152,7 +150,7 @@ export const Appearance = () => {
</Select>
</ListItem>
<NumberSetting
settingTitle={t('settings.label.manga_item_width')}
settingTitle={t`Manga item width`}
settingValue={`px: ${mangaGridItemWidth}`}
value={mangaGridItemWidth}
defaultValue={SERVER_SETTINGS_METADATA_DEFAULT.mangaGridItemWidth}
@@ -166,8 +164,8 @@ export const Appearance = () => {
<ListItem>
<ListItemText
primary={t('settings.appearance.manga_thumbnail_backdrop.title')}
secondary={t('settings.appearance.manga_thumbnail_backdrop.description')}
primary={t`Manga thumbnail as background`}
secondary={t`Sets the manga thumbnail as the background image on the manga page`}
/>
<Switch
edge="end"
@@ -178,8 +176,8 @@ export const Appearance = () => {
<ListItem>
<ListItemText
primary={t('settings.appearance.manga_dynamic_color_schemes.title')}
secondary={t('settings.appearance.manga_dynamic_color_schemes.description')}
primary={t`Dynamic theme colors on manga page`}
secondary={t`Changes the theme colors on the manga page based on the thumbnail`}
/>
<Switch
edge="end"

View File

@@ -6,15 +6,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro';
import {
IMAGE_PROCESSING_TYPE_TO_SETTING,
IMAGE_PROCESSING_TYPE_TO_TRANSLATION,
TARGET_DISABLED,
} from '@/features/settings/Settings.constants.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ImageProcessingType, ServerSettings } from '@/features/settings/Settings.types.ts';
@@ -37,7 +36,7 @@ import {
import { Processing } from '@/features/settings/components/images/Processing.tsx';
export const ImageProcessingSetting = ({ type }: { type: ImageProcessingType }) => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t(IMAGE_PROCESSING_TYPE_TO_TRANSLATION[type]));
@@ -53,7 +52,7 @@ export const ImageProcessingSetting = ({ type }: { type: ImageProcessingType })
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('ImageProcessingSetting::refetch'))}
/>
@@ -78,7 +77,7 @@ export const ImageProcessingSetting = ({ type }: { type: ImageProcessingType })
const updateSetting = (value: ServerSettings[typeof settingKey]): Promise<any> => {
const mutation = mutateSettings({ variables: { input: { settings: { [settingKey]: value } } } });
mutation.catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
mutation.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
return mutation;
};
@@ -93,7 +92,9 @@ export const ImageProcessingSetting = ({ type }: { type: ImageProcessingType })
return (
<Stack sx={{ p: 2, gap: 5 }}>
<Typography>{t('download.settings.conversion.description', { value: TARGET_DISABLED })}</Typography>
<Typography>
{t`In case no MIME-Type is defined, the "default" one will be used for the processing.\nSet the target mode to disabled to prevent processing for a MIME-Type`}
</Typography>
<Stack sx={{ flexDirection: 'column', gap: 5 }}>
{tmpConversions.map((conversion, index) => {
const { mimeType } = conversion;
@@ -140,10 +141,10 @@ export const ImageProcessingSetting = ({ type }: { type: ImageProcessingType })
]);
}}
>
{t('global.button.add')}
{t`Add`}
</Button>
<Button variant="contained" disabled={hasInvalidConversion || !hasChanged} onClick={onSubmit}>
{t('global.button.save')}
{t`Save`}
</Button>
</Stack>
</Stack>

View File

@@ -6,11 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import ListItemButton from '@mui/material/ListItemButton';
import { useLingui } from '@lingui/react/macro';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
@@ -20,18 +20,18 @@ import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
export const ImagesSettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.images.title'));
useAppTitle(t`Images`);
const [triggerClearServerCache, { loading: isClearingServerCache }] = requestManager.useClearServerCache();
const clearCache = async () => {
try {
await Promise.all([triggerClearServerCache(), ImageCache.clear()]);
makeToast(t('settings.clear_cache.label.success'), 'success');
makeToast(t`Cleared the cache`, 'success');
} catch (e) {
makeToast(t('settings.clear_cache.label.failure'), 'error', getErrorMessage(e));
makeToast(t`Could not clear the cache`, 'error', getErrorMessage(e));
}
};
@@ -39,22 +39,22 @@ export const ImagesSettings = () => {
<List sx={{ pt: 0 }}>
<ListItemButton disabled={isClearingServerCache} onClick={clearCache}>
<ListItemText
primary={t('settings.clear_cache.label.title')}
secondary={t('settings.clear_cache.label.description')}
primary={t`Clear cache`}
secondary={t`The cache of the client (browser, electron) should get cleared alongside it, otherwise, the client cache will keep getting used`}
/>
</ListItemButton>
<List
subheader={
<ListSubheader component="div" id="image-processing-settings">
{t('settings.images.processing.title')}
{t`Image processing`}
</ListSubheader>
}
>
<ListItemLink to={AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads.path}>
<ListItemText primary={t('download.settings.conversion.title')} />
<ListItemText primary={t`Image download processing`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.images.childRoutes.processingServe.path}>
<ListItemText primary={t('settings.images.processing.serve.title')} />
<ListItemText primary={t`Image serve processing`} />
</ListItemLink>
</List>
</List>

View File

@@ -7,12 +7,13 @@
*/
import { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListAltIcon from '@mui/icons-material/ListAlt';
import Divider from '@mui/material/Divider';
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { NAVIGATION_BAR_ITEMS } from '@/features/navigation-bar/NavigationBar.constants.ts';
@@ -23,10 +24,10 @@ import { NavbarItem, NavBarItemMoreGroup } from '@/features/navigation-bar/Navig
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export const More = () => {
const { t } = useTranslation();
const { t } = useLingui();
const isMobileWidth = MediaQuery.useIsMobileWidth();
useAppTitle(t('global.label.more'));
useAppTitle(t`More`);
const {
settings: { hideHistory },
@@ -46,7 +47,7 @@ export const More = () => {
...(hiddenNavBarItemsByMoreGroup[NavBarItemMoreGroup.HIDDEN_ITEM] ?? []),
{
path: AppRoutes.settings.childRoutes.categories.path,
title: 'category.title.category_other',
title: msg`Categories`,
SelectedIconComponent: ListAltIcon,
IconComponent: ListAltIcon,
show: 'both',

View File

@@ -6,7 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Trans, useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import Link from '@mui/material/Link';
import List from '@mui/material/List';
@@ -14,8 +13,9 @@ import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import ListSubheader from '@mui/material/ListSubheader';
import { t as translate } from 'i18next';
import { d } from 'koration';
import { Trans, useLingui } from '@lingui/react/macro';
import { plural, t as translate } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
@@ -44,16 +44,19 @@ import { KoreaderSyncSettings } from '@/features/settings/components/koreaderSyn
const getLogFilesCleanupDisplayValue = (ttl: number): string => {
if (ttl === 0) {
return translate('global.label.never');
return translate`Never`;
}
return translate('settings.server.misc.log_files.file_cleanup.value', { days: ttl, count: ttl });
return plural(ttl, {
one: 'Delete log files that are older than # day',
other: 'Delete log files that are older than # days',
});
};
export const ServerSettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.server.title.server'));
useAppTitle(t`Server`);
const {
settings: { serverInformAvailableUpdate },
@@ -62,7 +65,7 @@ export const ServerSettings = () => {
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<
keyof Pick<MetadataUpdateSettings, 'serverInformAvailableUpdate'>
>((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
>((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
const {
data,
@@ -85,7 +88,7 @@ export const ServerSettings = () => {
await mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
onCompletion?.(true);
} catch (e) {
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e));
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e));
onCompletion?.(false);
}
};
@@ -95,15 +98,15 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-client">
{t('global.label.client')}
{t`Client`}
</ListSubheader>
}
>
<ServerAddressSetting />
<ListItem>
<ListItemText
primary={t('global.update.settings.inform.label.title')}
secondary={t('global.update.settings.inform.label.description')}
primary={t`Inform about available update`}
secondary={t`Shows a dialog in case a new version is available`}
/>
<Switch
edge="end"
@@ -132,7 +135,7 @@ export const ServerSettings = () => {
<>
{localSettings}
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (metadataServerSettingsError) {
@@ -169,34 +172,34 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-server-address">
{t('settings.server.address.server.title')}
{t`Server bindings`}
</ListSubheader>
}
>
<TextSetting
settingName={t('settings.server.address.server.label.ip')}
settingName={t`IP`}
handleChange={(ip) => updateSetting('ip', ip)}
value={serverSettings.ip}
placeholder="0.0.0.0"
/>
<NumberSetting
settingTitle={t('settings.server.address.server.label.port')}
settingTitle={t`Port`}
settingValue={serverSettings.port.toString()}
handleUpdate={(port) => updateSetting('port', port)}
value={serverSettings.port}
defaultValue={4567}
valueUnit={t('settings.server.address.server.label.port')}
valueUnit={t`Port`}
/>
</List>
<List
subheader={
<ListSubheader component="div" id="server-settings-socks-proxy">
{t('settings.server.socks_proxy.title')}
{t`SOCKS proxy`}
</ListSubheader>
}
>
<ListItem>
<ListItemText primary={t('settings.server.socks_proxy.label.enable')} />
<ListItemText primary={t`Use SOCKS proxy`} />
<Switch
edge="end"
checked={serverSettings.socksProxyEnabled}
@@ -204,7 +207,7 @@ export const ServerSettings = () => {
/>
</ListItem>
<SelectSetting<number>
settingName={t('settings.server.socks_proxy.label.version')}
settingName={t`SOCKS version`}
value={serverSettings.socksProxyVersion}
values={[
[4, { text: '4' }],
@@ -213,22 +216,22 @@ export const ServerSettings = () => {
handleChange={(socksProxyVersion) => updateSetting('socksProxyVersion', socksProxyVersion)}
/>
<TextSetting
settingName={t('settings.server.socks_proxy.label.host')}
settingName={t`SOCKS host`}
value={serverSettings.socksProxyHost}
handleChange={(proxyHost) => updateSetting('socksProxyHost', proxyHost)}
/>
<TextSetting
settingName={t('settings.server.socks_proxy.label.port')}
settingName={t`SOCKS port`}
value={serverSettings.socksProxyPort}
handleChange={(proxyPort) => updateSetting('socksProxyPort', proxyPort)}
/>
<TextSetting
settingName={t('settings.server.socks_proxy.label.username')}
settingName={t`SOCKS username`}
value={serverSettings.socksProxyUsername}
handleChange={(proxyUsername) => updateSetting('socksProxyUsername', proxyUsername)}
/>
<TextSetting
settingName={t('settings.server.socks_proxy.label.password')}
settingName={t`SOCKS password`}
value={serverSettings.socksProxyPassword}
handleChange={(proxyPassword) => updateSetting('socksProxyPassword', proxyPassword)}
isPassword
@@ -237,12 +240,12 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-auth">
{t('settings.server.auth.title')}
{t`Authentication`}
</ListSubheader>
}
>
<SelectSetting<AuthMode>
settingName={t('settings.server.auth.label.title')}
settingName={t`Authentication Mode`}
value={serverSettings.authMode}
values={AUTH_MODES_SELECT_VALUES}
handleChange={(mode) => {
@@ -261,13 +264,13 @@ export const ServerSettings = () => {
disabled={authModeDisabled}
/>
<TextSetting
settingName={t('settings.server.auth.label.username')}
settingName={t`Username`}
value={serverSettings.authUsername}
validate={(value) => serverSettings.authMode === AuthMode.None || !!value.trim()}
handleChange={(authUsername) => updateSetting('authUsername', authUsername)}
/>
<TextSetting
settingName={t('settings.server.auth.label.password')}
settingName={t`Password`}
value={serverSettings.authPassword}
isPassword
validate={(value) => serverSettings.authMode === AuthMode.None || !!value.trim()}
@@ -276,15 +279,15 @@ export const ServerSettings = () => {
{serverSettings.authMode === AuthMode.UiLogin && (
<>
<TextSetting
settingName={t('settings.server.auth.jwt.audience')}
settingName={t`JWT audience claim`}
value={serverSettings.jwtAudience}
handleChange={(audience) => updateSetting('jwtAudience', audience)}
/>
<NumberSetting
settingTitle={t('settings.server.auth.jwt.access_token_expiry')}
settingTitle={t`JWT access token expiry`}
settingValue={d(serverSettings.jwtTokenExpiry).minutes.humanize()}
value={d(serverSettings.jwtTokenExpiry).minutes.inWholeMinutes}
valueUnit={t('global.time.minutes.minute_other')}
valueUnit={t`Minute`}
defaultValue={JWT_ACCESS_TOKEN_EXPIRY.default}
minValue={JWT_ACCESS_TOKEN_EXPIRY.min}
maxValue={JWT_ACCESS_TOKEN_EXPIRY.max}
@@ -292,10 +295,10 @@ export const ServerSettings = () => {
showSlider
/>
<NumberSetting
settingTitle={t('settings.server.auth.jwt.refresh_token_expiry')}
settingTitle={t`JWT refresh token expiry`}
settingValue={d(serverSettings.jwtRefreshExpiry).days.humanize()}
value={d(serverSettings.jwtRefreshExpiry).days.inWholeDays}
valueUnit={t('global.time.days.day_other')}
valueUnit={t`Day`}
defaultValue={JWT_REFRESH_TOKEN_EXPIRY.default}
minValue={JWT_REFRESH_TOKEN_EXPIRY.min}
maxValue={JWT_REFRESH_TOKEN_EXPIRY.max}
@@ -308,15 +311,15 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-clouadflare-bypass">
{t('settings.server.cloudflare.title')}
{t`Cloudflare bypass`}
</ListSubheader>
}
>
<ListItem>
<ListItemText
primary={t('settings.server.cloudflare.flaresolverr.enabled.label.title')}
primary={t`FlareSolverr enabled`}
secondary={
<Trans i18nKey="settings.server.cloudflare.flaresolverr.enabled.label.description">
<Trans>
See{' '}
<Link
href="https://github.com/FlareSolverr/FlareSolverr?tab=readme-ov-file#installation"
@@ -336,46 +339,52 @@ export const ServerSettings = () => {
/>
</ListItem>
<TextSetting
settingName={t('settings.server.cloudflare.flaresolverr.url.label.title')}
dialogDescription={t('settings.server.cloudflare.flaresolverr.url.label.description')}
settingName={t`FlareSolverr server url`}
dialogDescription={t`The address of the FlareSolverr server`}
value={serverSettings.flareSolverrUrl}
handleChange={(url) => updateSetting('flareSolverrUrl', url)}
/>
<NumberSetting
settingTitle={t('settings.server.cloudflare.flaresolverr.timeout.label.title')}
settingValue={t('global.time.seconds.value', { count: serverSettings.flareSolverrTimeout })}
dialogDescription={t('settings.server.cloudflare.flaresolverr.timeout.label.description')}
settingTitle={t`FlareSolverr request timeout`}
settingValue={plural(serverSettings.flareSolverrTimeout, {
one: '# second',
other: '# seconds',
})}
dialogDescription={t`How much time FlareSolverr has to handle the request`}
value={serverSettings.flareSolverrTimeout}
defaultValue={60}
minValue={20}
maxValue={60 * 5}
stepSize={1}
showSlider
valueUnit={t('global.time.seconds.second_other')}
valueUnit={t`Second`}
handleUpdate={(timeout) => updateSetting('flareSolverrTimeout', timeout)}
/>
<TextSetting
settingName={t('settings.server.cloudflare.flaresolverr.session.name.label.title')}
settingName={t`FlareSolverr session name`}
value={serverSettings.flareSolverrSessionName}
handleChange={(sessionName) => updateSetting('flareSolverrSessionName', sessionName)}
/>
<NumberSetting
settingTitle={t('settings.server.cloudflare.flaresolverr.session.ttl.label.title')}
settingValue={t('global.time.minutes.value', { count: serverSettings.flareSolverrSessionTtl })}
dialogDescription={t('settings.server.cloudflare.flaresolverr.session.ttl.label.description')}
settingTitle={t`FlareSolverr session TTL`}
settingValue={plural(serverSettings.flareSolverrSessionTtl, {
one: '# minute',
other: '# minutes',
})}
dialogDescription={t`FlareSolverr will automatically rotate expired sessions based on the TTL provided in minutes`}
value={serverSettings.flareSolverrSessionTtl}
defaultValue={15}
minValue={1}
maxValue={60}
stepSize={1}
showSlider
valueUnit={t('global.time.minutes.minute_other')}
valueUnit={t`Minute`}
handleUpdate={(sessionTTL) => updateSetting('flareSolverrSessionTtl', sessionTTL)}
/>
<ListItem>
<ListItemText
primary={t('settings.server.cloudflare.flaresolverr.response_fallback.label.title')}
secondary={t('settings.server.cloudflare.flaresolverr.response_fallback.label.description')}
primary={t`Response fallback`}
secondary={t`Use the FlareSolverr response in case the server runs into a Cloudflare challenge while FlareSolverr does not and is therefore unable to solve the challenge (does not work for images)`}
/>
<Switch
edge="end"
@@ -387,14 +396,14 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-opds">
{t('settings.server.opds.title')}
{t`OPDS`}
</ListSubheader>
}
>
<ListItem>
<ListItemText
primary={t('settings.server.opds.binary_file_sizes.label.title')}
secondary={t('settings.server.opds.binary_file_sizes.label.description')}
primary={t`Binary file size`}
secondary={t`Display file sizes in binary (KiB, MiB, GiB) instead of decimal (KB, MB, GB)`}
/>
<Switch
edge="end"
@@ -403,22 +412,22 @@ export const ServerSettings = () => {
/>
</ListItem>
<NumberSetting
settingTitle={t('settings.server.opds.items_per_page.label.title')}
settingTitle={t`Items per page`}
settingValue={serverSettings.opdsItemsPerPage.toString()}
dialogDescription={t('settings.server.opds.items_per_page.label.description')}
dialogDescription={t`Number of items per page in OPDS feeds (e.g., Library History, Manga Chapters).\nHigher values may affect client performance.`}
value={serverSettings.opdsItemsPerPage}
defaultValue={50}
minValue={10}
maxValue={5000}
stepSize={10}
showSlider
valueUnit={t('settings.server.opds.items_per_page.label.unit_other')}
valueUnit={t`item`}
handleUpdate={(value) => updateSetting('opdsItemsPerPage', value)}
/>
<ListItem>
<ListItemText
primary={t('settings.server.opds.enable_page_read_progress.label.title')}
secondary={t('settings.server.opds.enable_page_read_progress.label.description')}
primary={t`Enable page read progress`}
secondary={t`Track and update your reading progress by page for each chapter during page streaming`}
/>
<Switch
edge="end"
@@ -428,8 +437,8 @@ export const ServerSettings = () => {
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.server.opds.mark_as_read_on_download.label.title')}
secondary={t('settings.server.opds.mark_as_read_on_download.label.description')}
primary={t`Mark chapters as read on download`}
secondary={t`Automatically mark chapters as read when you download them.`}
/>
<Switch
edge="end"
@@ -439,8 +448,8 @@ export const ServerSettings = () => {
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.server.opds.show_only_unread_chapters.label.title')}
secondary={t('settings.server.opds.show_only_unread_chapters.label.description')}
primary={t`Show only unread chapters`}
secondary={t`Filter manga feed to display only chapters you havent read yet.`}
/>
<Switch
edge="end"
@@ -450,8 +459,8 @@ export const ServerSettings = () => {
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.server.opds.show_only_downloaded_chapters.label.title')}
secondary={t('settings.server.opds.show_only_downloaded_chapters.label.description')}
primary={t`Show only downloaded chapters`}
secondary={t`Filter manga feed to display only chapters you have downloaded.`}
/>
<Switch
edge="end"
@@ -460,23 +469,48 @@ export const ServerSettings = () => {
/>
</ListItem>
<SelectSetting<SortOrder>
settingName={t('settings.server.opds.chapter_sort_order.label.title')}
dialogDescription={t('settings.server.opds.chapter_sort_order.label.description')}
settingName={t`Chapter sort order`}
dialogDescription={t`Choose the order in which chapters are displayed.`}
value={serverSettings.opdsChapterSortOrder}
values={[
[SortOrder.Asc, { text: t('global.sort.label.asc') }],
[SortOrder.Desc, { text: t('global.sort.label.desc') }],
[
SortOrder.Asc,
{
text: t`Ascending`,
},
],
[
SortOrder.Desc,
{
text: t`Descending`,
},
],
]}
handleChange={(value) => updateSetting('opdsChapterSortOrder', value)}
/>
<SelectSetting<CbzMediaType>
settingName={t('settings.server.opds.cbz_mime_type.title')}
dialogDescription={t('settings.server.opds.cbz_mime_type.description')}
settingName={t`CBZ MIME-Type`}
dialogDescription={t`Controls the MimeType that Suwayomi sends in OPDS entries for CBZ archives. Also affects global CBZ download.\nModern follows recent IANA standard (2017), while LEGACY (deprecated mimetype for .cbz) and COMPATIBLE (deprecated mimetype for all comic archives) might be more compatible with older clients.`}
value={serverSettings.opdsCbzMimetype}
values={[
[CbzMediaType.Legacy, { text: t('settings.server.opds.cbz_mime_type.legacy') }],
[CbzMediaType.Modern, { text: t('settings.server.opds.cbz_mime_type.modern') }],
[CbzMediaType.Compatible, { text: t('settings.server.opds.cbz_mime_type.compatible') }],
[
CbzMediaType.Legacy,
{
text: t`Legacy`,
},
],
[
CbzMediaType.Modern,
{
text: t`Modern`,
},
],
[
CbzMediaType.Compatible,
{
text: t`Compatible`,
},
],
]}
handleChange={(value) => updateSetting('opdsCbzMimetype', value)}
/>
@@ -491,34 +525,44 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-database">
{t('settings.server.database.title')}
{t`Database`}
</ListSubheader>
}
>
<SelectSetting<DatabaseType>
settingName={t('settings.server.database.type.title')}
settingName={t`Type`}
value={serverSettings.databaseType}
values={[
[DatabaseType.H2, { text: t('settings.server.database.type.h2') }],
[DatabaseType.Postgresql, { text: t('settings.server.database.type.postgresql') }],
[
DatabaseType.H2,
{
text: t`H2`,
},
],
[
DatabaseType.Postgresql,
{
text: t`PostgreSQL`,
},
],
]}
handleChange={(value) => updateSetting('databaseType', value)}
/>
<TextSetting
settingName={t('settings.server.database.address')}
settingName={t`Database url`}
handleChange={(url) => updateSetting('databaseUrl', url)}
value={serverSettings.databaseUrl}
placeholder="postgresql://localhost:5432/suwayomi"
disabled={isH2Database}
/>
<TextSetting
settingName={t('settings.server.database.username')}
settingName={t`Username`}
value={serverSettings.databaseUsername}
handleChange={(username) => updateSetting('databaseUsername', username)}
disabled={isH2Database}
/>
<TextSetting
settingName={t('settings.server.database.password')}
settingName={t`Password`}
value={serverSettings.databasePassword}
handleChange={(password) => updateSetting('databasePassword', password)}
disabled={isH2Database}
@@ -526,8 +570,8 @@ export const ServerSettings = () => {
/>
<ListItem>
<ListItemText
primary={t('settings.server.database.hikari_connection_pool.title')}
secondary={t('settings.server.database.hikari_connection_pool.description')}
primary={t`Hikari connection pool`}
secondary={t`Improves performance, but may cause issues for some installations where data cannot be read from the database`}
/>
<Switch
edge="end"
@@ -539,12 +583,12 @@ export const ServerSettings = () => {
<List
subheader={
<ListSubheader component="div" id="server-settings-misc">
{t('settings.server.misc.title')}
{t`Misc`}
</ListSubheader>
}
>
<ListItem>
<ListItemText primary={t('settings.server.misc.log_level.label.server')} />
<ListItemText primary={t`Enable debug logs`} />
<Switch
edge="end"
checked={serverSettings.debugLogsEnabled}
@@ -553,8 +597,8 @@ export const ServerSettings = () => {
</ListItem>
<ListItem>
<ListItemText
primary={t('settings.server.misc.tray_icon.label.title')}
secondary={t('settings.server.misc.tray_icon.label.description')}
primary={t`Show icon in system tray`}
secondary={t`This icon will be shown on the system that is running the server`}
/>
<Switch
edge="end"
@@ -563,23 +607,23 @@ export const ServerSettings = () => {
/>
</ListItem>
<NumberSetting
settingTitle={t('settings.server.misc.log_files.file_cleanup.title')}
settingTitle={t`Log file cleanup`}
settingValue={getLogFilesCleanupDisplayValue(serverSettings.maxLogFiles)}
value={serverSettings.maxLogFiles}
valueUnit={t('global.date.label.day_one')}
valueUnit={t`Day`}
handleUpdate={(maxFiles) => updateSetting('maxLogFiles', maxFiles)}
/>
<TextSetting
settingName={t('settings.server.misc.log_files.file_size.title')}
settingName={t`Maximum log file size`}
value={serverSettings.maxLogFileSize}
dialogDescription={t('settings.server.misc.log_files.file_size.description')}
dialogDescription={t`Example for possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)`}
validate={(value) => !!value.match(/^[0-9]+(|kb|KB|mb|MB|gb|GB)$/g)}
handleChange={(maxLogFileSize) => updateSetting('maxLogFileSize', maxLogFileSize)}
/>
<TextSetting
settingName={t('settings.server.misc.log_files.total_size.title')}
settingName={t`Maximum size of all log files`}
value={serverSettings.maxLogFolderSize}
dialogDescription={t('settings.server.misc.log_files.total_size.description')}
dialogDescription={t`Example for possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)`}
validate={(value) => !!value.match(/^[0-9]+(|kb|KB|mb|MB|gb|GB)$/g)}
handleChange={(maxLogFolderSize) => updateSetting('maxLogFolderSize', maxLogFolderSize)}
/>

View File

@@ -11,7 +11,6 @@ import List from '@mui/material/List';
import BackupIcon from '@mui/icons-material/Backup';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useTranslation } from 'react-i18next';
import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined';
import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined';
import DnsIcon from '@mui/icons-material/Dns';
@@ -22,14 +21,15 @@ import SyncIcon from '@mui/icons-material/Sync';
import PaletteIcon from '@mui/icons-material/Palette';
import HistoryIcon from '@mui/icons-material/History';
import ImageIcon from '@mui/icons-material/Image';
import { useLingui } from '@lingui/react/macro';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export function Settings() {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.title'));
useAppTitle(t`Settings`);
return (
<List sx={{ padding: 0 }}>
@@ -37,74 +37,73 @@ export function Settings() {
<ListItemIcon>
<PaletteIcon />
</ListItemIcon>
<ListItemText primary={t('settings.appearance.title')} />
<ListItemText primary={t`Appearance`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.reader.path}>
<ListItemIcon>
<AutoStoriesIcon />
</ListItemIcon>
<ListItemText primary={t('reader.settings.title.reader')} />
<ListItemText primary={t`Reader`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.library.path}>
<ListItemIcon>
<CollectionsOutlinedBookmarkIcon />
</ListItemIcon>
<ListItemText primary={t('library.title')} />
<ListItemText primary={t`Library`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.download.path}>
<ListItemIcon>
<GetAppOutlinedIcon />
</ListItemIcon>
<ListItemText primary={t('download.title.download')} />
<ListItemText primary={t`Downloads`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.images.path}>
<ListItemIcon>
<ImageIcon />
</ListItemIcon>
<ListItemText primary={t('settings.images.title')} />
<ListItemText primary={t`Images`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.tracking.path}>
<ListItemIcon>
<SyncIcon />
</ListItemIcon>
<ListItemText primary={t('tracking.title')} />
<ListItemText primary={t`Tracking`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.backup.path}>
<ListItemIcon>
<BackupIcon />
</ListItemIcon>
<ListItemText primary={t('settings.backup.title')} />
<ListItemText primary={t`Backup`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.browse.path}>
<ListItemIcon>
<ExploreOutlinedIcon />
</ListItemIcon>
<ListItemText primary={t('global.label.browse')} />
<ListItemText primary={t`Browse`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.history.path}>
<ListItemIcon>
<HistoryIcon />
</ListItemIcon>
<ListItemText primary={t('history.title')} />
<ListItemText primary={t`History`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.device.path}>
<ListItemIcon>
<DevicesIcon />
</ListItemIcon>
<ListItemText primary={t('settings.device.title.device')} />
<ListItemText primary={t`Device`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.webui.path}>
<ListItemIcon>
<WebIcon />
</ListItemIcon>
<ListItemText primary={t('settings.webui.title.webui')} />
<ListItemText primary={t`WebUI`} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.server.path}>
<ListItemIcon>
<DnsIcon />
</ListItemIcon>
<ListItemText primary={t('settings.server.title.server')} />
<ListItemText primary={t`Server`} />
</ListItemLink>
</List>
);

View File

@@ -6,11 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { useLingui } from '@lingui/react/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { WebUIUpdateIntervalSetting } from '@/features/settings/components/webUI/WebUIUpdateIntervalSetting.tsx';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
@@ -35,9 +35,9 @@ import {
} from '@/features/settings/Settings.constants.ts';
export const WebUISettings = () => {
const { t } = useTranslation();
const { t } = useLingui();
useAppTitle(t('settings.webui.title.webui'));
useAppTitle(t`WebUI`);
const {
settings: { webUIInformAvailableUpdate },
@@ -46,7 +46,7 @@ export const WebUISettings = () => {
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<
keyof Pick<MetadataUpdateSettings, 'webUIInformAvailableUpdate'>
>((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
>((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
const {
data,
@@ -67,7 +67,7 @@ export const WebUISettings = () => {
}
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)),
);
};
@@ -80,7 +80,7 @@ export const WebUISettings = () => {
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (metadataServerSettingsError) {
@@ -105,13 +105,13 @@ export const WebUISettings = () => {
return (
<List sx={{ pt: 0 }}>
<SelectSetting<WebUiFlavor>
settingName={t('settings.webui.flavor.label.title')}
settingName={t`Flavor`}
value={webUISettings.webUIFlavor}
values={WEB_UI_FLAVOR_SELECT_VALUES}
handleChange={(flavor) => updateSetting('webUIFlavor', flavor)}
/>
<ListItem>
<ListItemText primary={t('settings.webui.label.initial_open_browser')} />
<ListItemText primary={t`Open the WebUI when starting the server`} />
<Switch
edge="end"
checked={webUISettings.initialOpenInBrowserEnabled}
@@ -119,22 +119,20 @@ export const WebUISettings = () => {
/>
</ListItem>
<SelectSetting<WebUiInterface>
settingName={t('settings.webui.interface.label.title')}
settingName={t`Interface`}
value={webUISettings.webUIInterface}
values={WEB_UI_INTERFACE_SELECT_VALUES}
handleChange={(webUIInterface) => updateSetting('webUIInterface', webUIInterface)}
/>
<TextSetting
settingName={t('settings.webui.electron_path.label.title')}
dialogDescription={t('settings.webui.electron_path.label.description')}
settingName={t`Electron path`}
dialogDescription={t`The path to the electron installation on the server`}
value={webUISettings.electronPath}
settingDescription={
webUISettings.electronPath.length ? webUISettings.electronPath : t('global.label.default')
}
settingDescription={webUISettings.electronPath.length ? webUISettings.electronPath : t`Default`}
handleChange={(path) => updateSetting('electronPath', path)}
/>
<SelectSetting<WebUiChannel>
settingName={t('settings.webui.channel.label.title')}
settingName={t`Channel`}
value={webUISettings.webUIChannel}
values={WEB_UI_CHANNEL_SELECT_VALUES}
handleChange={(channel) => updateSetting('webUIChannel', channel)}
@@ -147,8 +145,8 @@ export const WebUISettings = () => {
{!webUISettings.webUIUpdateCheckInterval && (
<ListItem>
<ListItemText
primary={t('global.update.settings.inform.label.title')}
secondary={t('global.update.settings.inform.label.description')}
primary={t`Inform about available update`}
secondary={t`Shows a dialog in case a new version is available`}
/>
<Switch
edge="end"