Feature/settings backup (#453)

* Show loading indicator for "NumberSetting"

* [i18n] Add lowercase interpolation format

* Add required dependencies for time settings

* Dynamically import dayjs locales for languages

* Add backup settings
This commit is contained in:
schroda
2023-11-12 01:41:10 +01:00
committed by GitHub
parent b080286b81
commit 9b96560a17
10 changed files with 417 additions and 68 deletions

View File

@@ -36,9 +36,11 @@
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5.14.16", "@mui/icons-material": "^5.14.16",
"@mui/material": "^5.14.16", "@mui/material": "^5.14.16",
"@mui/x-date-pickers": "^6.18.0",
"@vitejs/plugin-react-swc": "^3.4.1", "@vitejs/plugin-react-swc": "^3.4.1",
"apollo-upload-client": "^17.0.0", "apollo-upload-client": "^17.0.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"dayjs": "^1.11.10",
"file-selector": "^0.6.0", "file-selector": "^0.6.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"graphql-ws": "^5.14.2", "graphql-ws": "^5.14.2",

View File

@@ -22,7 +22,7 @@ import Slider from '@mui/material/Slider';
type BaseProps = { type BaseProps = {
settingTitle: string; settingTitle: string;
settingValue: string; settingValue?: string;
settingIcon?: React.ReactNode; settingIcon?: React.ReactNode;
value: number; value: number;
defaultValue?: number; defaultValue?: number;
@@ -98,7 +98,7 @@ export const NumberSetting = ({
{settingIcon ? <ListItemIcon>{settingIcon}</ListItemIcon> : null} {settingIcon ? <ListItemIcon>{settingIcon}</ListItemIcon> : null}
<ListItemText <ListItemText
primary={settingTitle} primary={settingTitle}
secondary={settingValue} secondary={settingValue ?? t('global.label.loading')}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }} secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/> />
</ListItemButton> </ListItemButton>

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Button, Dialog, DialogTitle, ListItemText } from '@mui/material';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import ListItemButton from '@mui/material/ListItemButton';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import dayjs from 'dayjs';
import { loadDayJsLocale } from '@/util/language.tsx';
export const TimeSetting = ({
settingName,
value,
defaultValue,
handleChange,
}: {
settingName: string;
value?: string;
defaultValue: string;
handleChange: (path: string) => void;
}) => {
const { t, i18n } = useTranslation();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(value ?? defaultValue);
const [locale, setLocale] = useState('en');
const currentLocale = i18n.language;
useEffect(() => {
loadDayJsLocale(currentLocale).then((wasLoaded) => setLocale(wasLoaded ? currentLocale : 'en'));
}, [currentLocale]);
useEffect(() => {
if (!value) {
return;
}
setDialogValue(value);
}, [value]);
const closeDialog = useCallback(
(resetValue: boolean) => {
setIsDialogOpen(false);
if (resetValue) {
setDialogValue(value ?? defaultValue);
}
},
[value],
);
const closeDialogWithReset = useCallback(() => closeDialog(true), [closeDialog]);
const updateSetting = useCallback(
(newValue: string, shouldCloseDialog: boolean = true) => {
if (shouldCloseDialog) {
closeDialog(false);
}
const didValueChange = value !== newValue;
if (!didValueChange) {
return;
}
handleChange(newValue);
},
[value, handleChange, closeDialog],
);
return (
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={settingName}
secondary={
value ? dayjs(value, 'HH:mm').locale(currentLocale).format('LT') : t('global.label.loading')
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogContent>
<DialogTitle sx={{ paddingLeft: 0 }}>{settingName}</DialogTitle>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={locale}>
<TimePicker
value={dayjs(dialogValue, 'HH:mm')}
defaultValue={dayjs(defaultValue, 'HH:mm')}
format="LT"
onChange={(time) => setDialogValue(time?.format('HH:mm') ?? '00:00')}
/>
</LocalizationProvider>
</DialogContent>
<DialogActions>
{defaultValue !== undefined ? (
<Button
onClick={() => {
setDialogValue(defaultValue);
updateSetting(defaultValue, false);
}}
color="primary"
>
{t('global.button.reset_to_default')}
</Button>
) : null}
<Button onClick={closeDialogWithReset} color="primary">
{t('global.button.cancel')}
</Button>
<Button
onClick={() => {
updateSetting(dialogValue);
}}
color="primary"
>
{t('global.button.ok')}
</Button>
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -21,7 +21,7 @@ export const DownloadAheadSetting = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings(); const { data } = requestManager.useGetServerSettings();
const downloadAheadLimit = data?.settings.autoDownloadAheadLimit ?? 0; const downloadAheadLimit = data?.settings.autoDownloadAheadLimit;
const shouldDownloadAhead = !!downloadAheadLimit; const shouldDownloadAhead = !!downloadAheadLimit;
const [mutateSettings] = requestManager.useUpdateServerSettings(); const [mutateSettings] = requestManager.useUpdateServerSettings();
@@ -49,10 +49,14 @@ export const DownloadAheadSetting = () => {
{shouldDownloadAhead ? ( {shouldDownloadAhead ? (
<NumberSetting <NumberSetting
settingTitle={t('download.settings.download_ahead.label.unread_chapters_to_download')} settingTitle={t('download.settings.download_ahead.label.unread_chapters_to_download')}
settingValue={t('download.settings.download_ahead.label.value', { settingValue={
chapters: downloadAheadLimit, downloadAheadLimit
count: downloadAheadLimit, ? t('download.settings.download_ahead.label.value', {
})} chapters: downloadAheadLimit,
count: downloadAheadLimit,
})
: undefined
}
value={downloadAheadLimit} value={downloadAheadLimit}
minValue={MIN_LIMIT} minValue={MIN_LIMIT}
maxValue={MAX_LIMIT} maxValue={MAX_LIMIT}

View File

@@ -21,7 +21,7 @@ export const GlobalUpdateSettingsInterval = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = requestManager.useGetServerSettings(); const { data } = requestManager.useGetServerSettings();
const autoUpdateIntervalHours = data?.settings.globalUpdateInterval ?? 0; const autoUpdateIntervalHours = data?.settings.globalUpdateInterval;
const doAutoUpdates = !!autoUpdateIntervalHours; const doAutoUpdates = !!autoUpdateIntervalHours;
const [mutateSettings] = requestManager.useUpdateServerSettings(); const [mutateSettings] = requestManager.useUpdateServerSettings();
@@ -45,9 +45,13 @@ export const GlobalUpdateSettingsInterval = () => {
{doAutoUpdates ? ( {doAutoUpdates ? (
<NumberSetting <NumberSetting
settingTitle={t('library.settings.global_update.auto_update.interval.label.title')} settingTitle={t('library.settings.global_update.auto_update.interval.label.title')}
settingValue={t('library.settings.global_update.auto_update.interval.label.value', { settingValue={
hours: autoUpdateIntervalHours, autoUpdateIntervalHours
})} ? t('library.settings.global_update.auto_update.interval.label.value', {
hours: autoUpdateIntervalHours,
})
: undefined
}
value={autoUpdateIntervalHours} value={autoUpdateIntervalHours}
minValue={MIN_INTERVAL_HOURS} minValue={MIN_INTERVAL_HOURS}
maxValue={MAX_INTERVAL_HOURS} maxValue={MAX_INTERVAL_HOURS}

View File

@@ -19,6 +19,14 @@ export const i18n = use(initReactI18next)
fallbackLng: 'en', fallbackLng: 'en',
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
format: (value, format) => {
switch (format) {
case 'lowercase':
return value.toLowerCase();
default:
return value;
}
},
}, },
returnNull: false, returnNull: false,
debug: process.env.NODE_ENV !== 'production', debug: process.env.NODE_ENV !== 'production',

View File

@@ -243,10 +243,18 @@
}, },
"date": { "date": {
"label": { "label": {
"day": "Day",
"day_one": "Day",
"day_other": "Days",
"today": "Today", "today": "Today",
"today_at": "Today at {{timeString}}", "today_at": "Today at {{timeString}}",
"yesterday": "Yesterday", "yesterday": "Yesterday",
"yesterday_at": "Yesterday at {{timeString}}" "yesterday_at": "Yesterday at {{timeString}}"
},
"value": {
"label": {
"day": "{{days}} $t(global.date.label.day, lowercase)"
}
} }
}, },
"error": { "error": {
@@ -278,6 +286,7 @@
"display": "Display", "display": "Display",
"filter": "Filter", "filter": "Filter",
"loading": "Loading…", "loading": "Loading…",
"never": "Never",
"none": "None", "none": "None",
"sort": "Sort", "sort": "Sort",
"unknown": "Unknown" "unknown": "Unknown"
@@ -486,6 +495,24 @@
"title": "About" "title": "About"
}, },
"backup": { "backup": {
"automated": {
"cleanup": {
"label": {
"title": "Backup cleanup",
"value": "Delete backups that are older than $t(global.date.value.label.day)"
}
},
"label": {
"interval": "Backup interval",
"time": "Backup time"
},
"location": {
"label": {
"description": "The path to the directory on the server where automated backups should get saved in",
"title": "Backup location"
}
}
},
"label": { "label": {
"backup_restore_failed": "Could not restore backup", "backup_restore_failed": "Could not restore backup",
"create_backup": "Create backup", "create_backup": "Create backup",

View File

@@ -13,12 +13,39 @@ import { fromEvent } from 'file-selector';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ListItemButton } from '@mui/material'; import { ListItemButton } from '@mui/material';
import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemIcon from '@mui/material/ListItemIcon';
import ListSubheader from '@mui/material/ListSubheader';
import { t as translate } from 'i18next';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/components/util/Toast'; import { makeToast } from '@/components/util/Toast';
import { ListItemLink } from '@/components/util/ListItemLink'; import { ListItemLink } from '@/components/util/ListItemLink';
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext';
import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts'; import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts';
import { Progress } from '@/components/util/Progress.tsx'; import { Progress } from '@/components/util/Progress.tsx';
import { ServerDirSetting } from '@/components/settings/ServerDirSetting.tsx';
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
import { TimeSetting } from '@/components/settings/TimeSetting.tsx';
import { ServerSettings } from '@/typings.ts';
type BackupSettingsType = Pick<ServerSettings, 'backupPath' | 'backupTime' | 'backupInterval' | 'backupTTL'>;
const extractBackupSettings = (settings: ServerSettings): BackupSettingsType => ({
backupPath: settings.backupPath,
backupTime: settings.backupTime,
backupInterval: settings.backupInterval,
backupTTL: settings.backupTTL,
});
const getBackupCleanupDisplayValue = (ttl?: number) => {
if (ttl === undefined) {
return undefined;
}
if (ttl === 0) {
return translate('global.label.never');
}
return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl });
};
let backupRestoreId: string | undefined; let backupRestoreId: string | undefined;
@@ -32,6 +59,11 @@ export function Backup() {
useSetDefaultBackTo('settings'); useSetDefaultBackTo('settings');
const { data: settingsData } = requestManager.useGetServerSettings();
const [mutateSettings] = requestManager.useUpdateServerSettings();
const backupSettings = settingsData ? extractBackupSettings(settingsData.settings) : undefined;
const { data } = requestManager.useGetBackupRestoreStatus(backupRestoreId ?? '', { const { data } = requestManager.useGetBackupRestoreStatus(backupRestoreId ?? '', {
skip: !backupRestoreId, skip: !backupRestoreId,
pollInterval: 1000, pollInterval: 1000,
@@ -48,6 +80,13 @@ export function Backup() {
return Number.isNaN(progress) ? 0 : progress; return Number.isNaN(progress) ? 0 : progress;
})(); })();
const updateSetting = <Setting extends keyof BackupSettingsType>(
setting: Setting,
value: BackupSettingsType[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
};
useEffect(() => { useEffect(() => {
if (!data?.restoreStatus) { if (!data?.restoreStatus) {
return; return;
@@ -142,6 +181,59 @@ export function Backup() {
</ListItemIcon> </ListItemIcon>
) : null} ) : null}
</ListItemButton> </ListItemButton>
<List
subheader={
<ListSubheader component="div" id="backup-settings">
Automated backup
</ListSubheader>
}
>
<ServerDirSetting
settingName={t('settings.backup.automated.location.label.title')}
dialogDescription={t('settings.backup.automated.location.label.description')}
dirPath={backupSettings?.backupPath}
handlePathChange={(path) => updateSetting('backupPath', path)}
/>
<TimeSetting
settingName={t('settings.backup.automated.label.time')}
value={backupSettings?.backupTime}
defaultValue="00:00"
handleChange={(time: string) => updateSetting('backupTime', time)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.label.interval')}
settingValue={
backupSettings?.backupInterval
? t('global.date.value.label.day', {
days: backupSettings.backupInterval,
count: backupSettings.backupInterval,
})
: undefined
}
value={backupSettings?.backupInterval ?? 1}
defaultValue={1}
minValue={1}
maxValue={31}
stepSize={1}
dialogTitle="Interval"
valueUnit="Day"
showSlider
handleUpdate={(interval: number) => updateSetting('backupInterval', interval)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.cleanup.label.title')}
settingValue={getBackupCleanupDisplayValue(backupSettings?.backupTTL)}
value={backupSettings?.backupTTL ?? 14}
defaultValue={14}
minValue={0}
maxValue={1000}
stepSize={1}
dialogTitle="Cleanup"
valueUnit="Day"
showSlider
handleUpdate={(ttl: number) => updateSetting('backupTTL', ttl)}
/>
</List>
</List> </List>
<input type="file" id="backup-file" style={{ display: 'none' }} /> <input type="file" id="backup-file" style={{ display: 'none' }} />
</> </>

View File

@@ -14,67 +14,88 @@ export enum DefaultLanguage {
LOCAL_SOURCE = 'localsourcelang', LOCAL_SOURCE = 'localsourcelang',
} }
const loadDefaultDayJsLocale = () => import('dayjs/locale/en.js');
export const ISOLanguages = [ export const ISOLanguages = [
// full list: https://github.com/meikidd/iso-639-1/blob/master/src/data.js // full list: https://github.com/meikidd/iso-639-1/blob/master/src/data.js
{ code: 'en', name: 'English', nativeName: 'English' }, { dayjsImport: loadDefaultDayJsLocale, code: 'en', name: 'English', nativeName: 'English' },
{ code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' }, { dayjsImport: () => import('dayjs/locale/ca.js'), code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' },
{ code: 'de', name: 'German', nativeName: 'Deutsch' }, { dayjsImport: () => import('dayjs/locale/de.js'), code: 'de', name: 'German', nativeName: 'Deutsch' },
{ code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' }, { dayjsImport: () => import('dayjs/locale/es.js'), code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' },
{ code: 'es-419', name: 'Spanish; Castilian', nativeName: 'Español (Latinoamérica)' }, {
{ code: 'fr', name: 'French', nativeName: 'Français' }, dayjsImport: () => import('dayjs/locale/es.js'),
{ code: 'id', name: 'Indonesian', nativeName: 'Indonesia' }, code: 'es-419',
{ code: 'it', name: 'Italian', nativeName: 'Italiano' }, name: 'Spanish; Castilian',
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' }, nativeName: 'Español (Latinoamérica)',
{ code: 'pt-pt', name: 'Portuguese', nativeName: 'Português (Portugal)' }, },
{ code: 'pt-br', name: 'Portuguese; Brasil', nativeName: 'Português (Brasil)' }, { dayjsImport: () => import('dayjs/locale/fr.js'), code: 'fr', name: 'French', nativeName: 'Français' },
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' }, { dayjsImport: () => import('dayjs/locale/id.js'), code: 'id', name: 'Indonesian', nativeName: 'Indonesia' },
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe' }, { dayjsImport: () => import('dayjs/locale/it.js'), code: 'it', name: 'Italian', nativeName: 'Italiano' },
{ code: 'ru', name: 'Russian', nativeName: 'русский' }, { dayjsImport: () => import('dayjs/locale/pt.js'), code: 'pt', name: 'Portuguese', nativeName: 'Português' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' }, {
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' }, dayjsImport: () => import('dayjs/locale/pt.js'),
{ code: 'th', name: 'Thai', nativeName: 'ไทย' }, code: 'pt-pt',
{ code: 'zh', name: 'Chinese', nativeName: '中文' }, name: 'Portuguese',
{ code: 'zh-hans', name: 'Chinese', nativeName: '中文 (HANS)' }, nativeName: 'Português (Portugal)',
{ code: 'zh-hant', name: 'Chinese', nativeName: '中文 (HANT)' }, },
{ code: 'zh-rhk', name: 'Chinese', nativeName: '中文 (RHK)' }, {
{ code: 'zh-rtw', name: 'Chinese', nativeName: '中文 (RTW)' }, dayjsImport: () => import('dayjs/locale/pt-br.js'),
code: 'pt-br',
name: 'Portuguese; Brasil',
nativeName: 'Português (Brasil)',
},
{ dayjsImport: () => import('dayjs/locale/vi.js'), code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' },
{ dayjsImport: () => import('dayjs/locale/tr.js'), code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
{ dayjsImport: () => import('dayjs/locale/ru.js'), code: 'ru', name: 'Russian', nativeName: 'русский' },
{ dayjsImport: () => import('dayjs/locale/ar.js'), code: 'ar', name: 'Arabic', nativeName: 'العربية' },
{ dayjsImport: () => import('dayjs/locale/hi.js'), code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
{ dayjsImport: () => import('dayjs/locale/th.js'), code: 'th', name: 'Thai', nativeName: 'ไทย' },
{ dayjsImport: () => import('dayjs/locale/zh.js'), code: 'zh', name: 'Chinese', nativeName: '中文' },
{ dayjsImport: () => import('dayjs/locale/zh-cn.js'), code: 'zh-hans', name: 'Chinese', nativeName: '中文 (HANS)' },
{ dayjsImport: () => import('dayjs/locale/zh-hk.js'), code: 'zh-hant', name: 'Chinese', nativeName: '中文 (HANT)' },
{ dayjsImport: () => import('dayjs/locale/zh-hk.js'), code: 'zh-rhk', name: 'Chinese', nativeName: '中文 (RHK)' },
{ dayjsImport: () => import('dayjs/locale/zh-tw.js'), code: 'zh-rtw', name: 'Chinese', nativeName: '中文 (RTW)' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語' }, { dayjsImport: () => import('dayjs/locale/ja.js'), code: 'ja', name: 'Japanese', nativeName: '日本語' },
{ code: 'ko', name: 'Korean', nativeName: '한국어' }, { dayjsImport: () => import('dayjs/locale/ko.js'), code: 'ko', name: 'Korean', nativeName: '한국어' },
{ code: 'zu', name: 'Zulu', nativeName: 'isiZulu' }, { dayjsImport: () => import('dayjs/locale/af.js'), code: 'zu', name: 'Zulu', nativeName: 'isiZulu' },
{ code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' }, { dayjsImport: () => import('dayjs/locale/af.js'), code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' },
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська' }, { dayjsImport: () => import('dayjs/locale/uk.js'), code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
{ code: 'ro', name: 'Romanian', nativeName: 'Română' }, { dayjsImport: () => import('dayjs/locale/ro.js'), code: 'ro', name: 'Romanian', nativeName: 'Română' },
{ code: 'bg', name: 'Bulgarian', nativeName: 'български' }, { dayjsImport: () => import('dayjs/locale/bg.js'), code: 'bg', name: 'Bulgarian', nativeName: 'български' },
{ code: 'cs', name: 'Czech', nativeName: 'čeština' }, { dayjsImport: () => import('dayjs/locale/cs.js'), code: 'cs', name: 'Czech', nativeName: 'čeština' },
{ code: 'pl', name: 'Polish', nativeName: 'polski' }, { dayjsImport: () => import('dayjs/locale/pl.js'), code: 'pl', name: 'Polish', nativeName: 'polski' },
{ code: 'no', name: 'Norwegian', nativeName: 'Norsk' }, { dayjsImport: () => import('dayjs/locale/nb.js'), code: 'no', name: 'Norwegian', nativeName: 'Norsk' },
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' }, { dayjsImport: () => import('dayjs/locale/nl.js'), code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
{ code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' }, { dayjsImport: () => import('dayjs/locale/my.js'), code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' },
{ code: 'ms', name: 'Malay', nativeName: 'Malaysia' }, { dayjsImport: () => import('dayjs/locale/ms.js'), code: 'ms', name: 'Malay', nativeName: 'Malaysia' },
{ code: 'mn', name: 'Mongolian', nativeName: 'Монгол' }, { dayjsImport: () => import('dayjs/locale/mn.js'), code: 'mn', name: 'Mongolian', nativeName: 'Монгол' },
{ code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' }, { dayjsImport: () => import('dayjs/locale/ml.js'), code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
{ code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' }, { dayjsImport: () => import('dayjs/locale/ku.js'), code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' },
{ code: 'hu', name: 'Hungarian', nativeName: 'Magyar' }, { dayjsImport: () => import('dayjs/locale/hu.js'), code: 'hu', name: 'Hungarian', nativeName: 'Magyar' },
{ code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' }, { dayjsImport: () => import('dayjs/locale/hr.js'), code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' },
{ code: 'he', name: 'Hebrew', nativeName: 'עברית' }, { dayjsImport: () => import('dayjs/locale/he.js'), code: 'he', name: 'Hebrew', nativeName: 'עברית' },
{ code: 'fil', name: 'Filipino', nativeName: 'Filipino' }, { dayjsImport: () => import('dayjs/locale/tl-ph'), code: 'fil', name: 'Filipino', nativeName: 'Filipino' },
{ code: 'fi', name: 'Finnish', nativeName: 'suomi' }, { dayjsImport: () => import('dayjs/locale/fi.js'), code: 'fi', name: 'Finnish', nativeName: 'suomi' },
{ code: 'fa', name: 'Persian', nativeName: 'فارسی' }, { dayjsImport: () => import('dayjs/locale/fa.js'), code: 'fa', name: 'Persian', nativeName: 'فارسی' },
{ code: 'eu', name: 'Basque', nativeName: 'euskara' }, { dayjsImport: () => import('dayjs/locale/eu.js'), code: 'eu', name: 'Basque', nativeName: 'euskara' },
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά' }, { dayjsImport: () => import('dayjs/locale/el.js'), code: 'el', name: 'Greek', nativeName: 'Ελληνικά' },
{ code: 'da', name: 'Danish', nativeName: 'dansk' }, { dayjsImport: () => import('dayjs/locale/da.js'), code: 'da', name: 'Danish', nativeName: 'dansk' },
{ code: 'bn', name: 'Bengali', nativeName: 'বাংলা' }, { dayjsImport: () => import('dayjs/locale/bn.js'), code: 'bn', name: 'Bengali', nativeName: 'বাংলা' },
{ code: 'lt', name: 'Lithuanian', nativeName: 'lietuvių kalba' }, { dayjsImport: () => import('dayjs/locale/lt.js'), code: 'lt', name: 'Lithuanian', nativeName: 'lietuvių kalba' },
{ code: 'sh', name: 'Serbo-Croatian', nativeName: 'srpskohrvatski' }, {
dayjsImport: loadDefaultDayJsLocale,
code: 'sh',
name: 'Serbo-Croatian',
nativeName: 'srpskohrvatski',
},
{ code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' }, { dayjsImport: () => import('dayjs/locale/af.js'), code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' },
{ code: 'am', name: 'Amharic', nativeName: 'አማርኛ' }, { dayjsImport: () => import('dayjs/locale/am.js'), code: 'am', name: 'Amharic', nativeName: 'አማርኛ' },
{ code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' }, { dayjsImport: () => import('dayjs/locale/az.js'), code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' },
{ code: 'be', name: 'Belarusian', nativeName: 'беларуская' }, { dayjsImport: () => import('dayjs/locale/be.js'), code: 'be', name: 'Belarusian', nativeName: 'беларуская' },
{ code: 'bs', name: 'Bosnian', nativeName: 'bosanski' }, { dayjsImport: () => import('dayjs/locale/bs.js'), code: 'bs', name: 'Bosnian', nativeName: 'bosanski' },
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' }, { dayjsImport: () => import('dayjs/locale/sv.js'), code: 'sv', name: 'Swedish', nativeName: 'svenska' },
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
]; ];
export function langCodeToName(code: string): string { export function langCodeToName(code: string): string {
@@ -120,3 +141,20 @@ export const langSortCmp = (a: string, b: string) => {
return aLang > bLang ? 1 : -1; return aLang > bLang ? 1 : -1;
}; };
export const loadDayJsLocale = async (locale: string) => {
const lang = ISOLanguages.find(({ code }) => code.toLowerCase() === locale.toLowerCase());
try {
if (!lang) {
await loadDefaultDayJsLocale();
return false;
}
await lang.dayjsImport();
return true;
} catch (e) {
return false;
// ignore - dayjs falls back to en anyway
}
};

View File

@@ -1400,6 +1400,19 @@
clsx "^2.0.0" clsx "^2.0.0"
prop-types "^15.8.1" prop-types "^15.8.1"
"@mui/base@^5.0.0-beta.22":
version "5.0.0-beta.23"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.23.tgz#dd10dfc609d8937749521f940965f757fa3c0f2c"
integrity sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw==
dependencies:
"@babel/runtime" "^7.23.2"
"@floating-ui/react-dom" "^2.0.2"
"@mui/types" "^7.2.8"
"@mui/utils" "^5.14.17"
"@popperjs/core" "^2.11.8"
clsx "^2.0.0"
prop-types "^15.8.1"
"@mui/core-downloads-tracker@^5.14.16": "@mui/core-downloads-tracker@^5.14.16":
version "5.14.16" version "5.14.16"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.16.tgz#03ceb422d69a33e6c1cbd7e943cf60816878be2a" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.16.tgz#03ceb422d69a33e6c1cbd7e943cf60816878be2a"
@@ -1478,6 +1491,29 @@
prop-types "^15.8.1" prop-types "^15.8.1"
react-is "^18.2.0" react-is "^18.2.0"
"@mui/utils@^5.14.17":
version "5.14.17"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.17.tgz#8e2e7ca58865119eec8c6bdb359f539c25aaf576"
integrity sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==
dependencies:
"@babel/runtime" "^7.23.2"
"@types/prop-types" "^15.7.9"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/x-date-pickers@^6.18.0":
version "6.18.0"
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.18.0.tgz#3bc439d246315e957858f08a8c4a3a8fbe95adfd"
integrity sha512-y4UlkHQXiNRfb6FWQ/GWir0sZ+9kL+GEEZssG+XWP3KJ+d3lONRteusl4AJkYJBdIAOh+5LnMV9RAQKq9Sl7yw==
dependencies:
"@babel/runtime" "^7.23.2"
"@mui/base" "^5.0.0-beta.22"
"@mui/utils" "^5.14.16"
"@types/react-transition-group" "^4.4.8"
clsx "^2.0.0"
prop-types "^15.8.1"
react-transition-group "^4.4.5"
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -2634,6 +2670,11 @@ dataloader@^2.2.2:
resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0" resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0"
integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g== integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
debounce@^1.2.0: debounce@^1.2.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"