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:
@@ -36,9 +36,11 @@
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mui/icons-material": "^5.14.16",
|
||||
"@mui/material": "^5.14.16",
|
||||
"@mui/x-date-pickers": "^6.18.0",
|
||||
"@vitejs/plugin-react-swc": "^3.4.1",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"axios": "^1.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"file-selector": "^0.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.14.2",
|
||||
|
||||
@@ -22,7 +22,7 @@ import Slider from '@mui/material/Slider';
|
||||
|
||||
type BaseProps = {
|
||||
settingTitle: string;
|
||||
settingValue: string;
|
||||
settingValue?: string;
|
||||
settingIcon?: React.ReactNode;
|
||||
value: number;
|
||||
defaultValue?: number;
|
||||
@@ -98,7 +98,7 @@ export const NumberSetting = ({
|
||||
{settingIcon ? <ListItemIcon>{settingIcon}</ListItemIcon> : null}
|
||||
<ListItemText
|
||||
primary={settingTitle}
|
||||
secondary={settingValue}
|
||||
secondary={settingValue ?? t('global.label.loading')}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
133
src/components/settings/TimeSetting.tsx
Normal file
133
src/components/settings/TimeSetting.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,7 @@ export const DownloadAheadSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = requestManager.useGetServerSettings();
|
||||
const downloadAheadLimit = data?.settings.autoDownloadAheadLimit ?? 0;
|
||||
const downloadAheadLimit = data?.settings.autoDownloadAheadLimit;
|
||||
const shouldDownloadAhead = !!downloadAheadLimit;
|
||||
const [mutateSettings] = requestManager.useUpdateServerSettings();
|
||||
|
||||
@@ -49,10 +49,14 @@ export const DownloadAheadSetting = () => {
|
||||
{shouldDownloadAhead ? (
|
||||
<NumberSetting
|
||||
settingTitle={t('download.settings.download_ahead.label.unread_chapters_to_download')}
|
||||
settingValue={t('download.settings.download_ahead.label.value', {
|
||||
chapters: downloadAheadLimit,
|
||||
count: downloadAheadLimit,
|
||||
})}
|
||||
settingValue={
|
||||
downloadAheadLimit
|
||||
? t('download.settings.download_ahead.label.value', {
|
||||
chapters: downloadAheadLimit,
|
||||
count: downloadAheadLimit,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
value={downloadAheadLimit}
|
||||
minValue={MIN_LIMIT}
|
||||
maxValue={MAX_LIMIT}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const GlobalUpdateSettingsInterval = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = requestManager.useGetServerSettings();
|
||||
const autoUpdateIntervalHours = data?.settings.globalUpdateInterval ?? 0;
|
||||
const autoUpdateIntervalHours = data?.settings.globalUpdateInterval;
|
||||
const doAutoUpdates = !!autoUpdateIntervalHours;
|
||||
const [mutateSettings] = requestManager.useUpdateServerSettings();
|
||||
|
||||
@@ -45,9 +45,13 @@ export const GlobalUpdateSettingsInterval = () => {
|
||||
{doAutoUpdates ? (
|
||||
<NumberSetting
|
||||
settingTitle={t('library.settings.global_update.auto_update.interval.label.title')}
|
||||
settingValue={t('library.settings.global_update.auto_update.interval.label.value', {
|
||||
hours: autoUpdateIntervalHours,
|
||||
})}
|
||||
settingValue={
|
||||
autoUpdateIntervalHours
|
||||
? t('library.settings.global_update.auto_update.interval.label.value', {
|
||||
hours: autoUpdateIntervalHours,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
value={autoUpdateIntervalHours}
|
||||
minValue={MIN_INTERVAL_HOURS}
|
||||
maxValue={MAX_INTERVAL_HOURS}
|
||||
|
||||
@@ -19,6 +19,14 @@ export const i18n = use(initReactI18next)
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
format: (value, format) => {
|
||||
switch (format) {
|
||||
case 'lowercase':
|
||||
return value.toLowerCase();
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
},
|
||||
},
|
||||
returnNull: false,
|
||||
debug: process.env.NODE_ENV !== 'production',
|
||||
|
||||
@@ -243,10 +243,18 @@
|
||||
},
|
||||
"date": {
|
||||
"label": {
|
||||
"day": "Day",
|
||||
"day_one": "Day",
|
||||
"day_other": "Days",
|
||||
"today": "Today",
|
||||
"today_at": "Today at {{timeString}}",
|
||||
"yesterday": "Yesterday",
|
||||
"yesterday_at": "Yesterday at {{timeString}}"
|
||||
},
|
||||
"value": {
|
||||
"label": {
|
||||
"day": "{{days}} $t(global.date.label.day, lowercase)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -278,6 +286,7 @@
|
||||
"display": "Display",
|
||||
"filter": "Filter",
|
||||
"loading": "Loading…",
|
||||
"never": "Never",
|
||||
"none": "None",
|
||||
"sort": "Sort",
|
||||
"unknown": "Unknown"
|
||||
@@ -486,6 +495,24 @@
|
||||
"title": "About"
|
||||
},
|
||||
"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": {
|
||||
"backup_restore_failed": "Could not restore backup",
|
||||
"create_backup": "Create backup",
|
||||
|
||||
@@ -13,12 +13,39 @@ import { fromEvent } from 'file-selector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ListItemButton } from '@mui/material';
|
||||
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 { makeToast } from '@/components/util/Toast';
|
||||
import { ListItemLink } from '@/components/util/ListItemLink';
|
||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts';
|
||||
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;
|
||||
|
||||
@@ -32,6 +59,11 @@ export function Backup() {
|
||||
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
const { data: settingsData } = requestManager.useGetServerSettings();
|
||||
const [mutateSettings] = requestManager.useUpdateServerSettings();
|
||||
|
||||
const backupSettings = settingsData ? extractBackupSettings(settingsData.settings) : undefined;
|
||||
|
||||
const { data } = requestManager.useGetBackupRestoreStatus(backupRestoreId ?? '', {
|
||||
skip: !backupRestoreId,
|
||||
pollInterval: 1000,
|
||||
@@ -48,6 +80,13 @@ export function Backup() {
|
||||
return Number.isNaN(progress) ? 0 : progress;
|
||||
})();
|
||||
|
||||
const updateSetting = <Setting extends keyof BackupSettingsType>(
|
||||
setting: Setting,
|
||||
value: BackupSettingsType[Setting],
|
||||
) => {
|
||||
mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.restoreStatus) {
|
||||
return;
|
||||
@@ -142,6 +181,59 @@ export function Backup() {
|
||||
</ListItemIcon>
|
||||
) : null}
|
||||
</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>
|
||||
<input type="file" id="backup-file" style={{ display: 'none' }} />
|
||||
</>
|
||||
|
||||
@@ -14,67 +14,88 @@ export enum DefaultLanguage {
|
||||
LOCAL_SOURCE = 'localsourcelang',
|
||||
}
|
||||
|
||||
const loadDefaultDayJsLocale = () => import('dayjs/locale/en.js');
|
||||
|
||||
export const ISOLanguages = [
|
||||
// full list: https://github.com/meikidd/iso-639-1/blob/master/src/data.js
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
{ 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' },
|
||||
{ code: 'id', name: 'Indonesian', nativeName: 'Indonesia' },
|
||||
{ code: 'it', name: 'Italian', nativeName: 'Italiano' },
|
||||
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
|
||||
{ code: 'pt-pt', name: 'Portuguese', nativeName: 'Português (Portugal)' },
|
||||
{ code: 'pt-br', name: 'Portuguese; Brasil', nativeName: 'Português (Brasil)' },
|
||||
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' },
|
||||
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
|
||||
{ code: 'ru', name: 'Russian', nativeName: 'русский' },
|
||||
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
|
||||
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
|
||||
{ code: 'th', name: 'Thai', nativeName: 'ไทย' },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
|
||||
{ code: 'zh-hans', name: 'Chinese', nativeName: '中文 (HANS)' },
|
||||
{ code: 'zh-hant', name: 'Chinese', nativeName: '中文 (HANT)' },
|
||||
{ code: 'zh-rhk', name: 'Chinese', nativeName: '中文 (RHK)' },
|
||||
{ code: 'zh-rtw', name: 'Chinese', nativeName: '中文 (RTW)' },
|
||||
{ dayjsImport: loadDefaultDayJsLocale, code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ca.js'), code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' },
|
||||
{ dayjsImport: () => import('dayjs/locale/de.js'), code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
{ dayjsImport: () => import('dayjs/locale/es.js'), code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' },
|
||||
{
|
||||
dayjsImport: () => import('dayjs/locale/es.js'),
|
||||
code: 'es-419',
|
||||
name: 'Spanish; Castilian',
|
||||
nativeName: 'Español (Latinoamérica)',
|
||||
},
|
||||
{ dayjsImport: () => import('dayjs/locale/fr.js'), code: 'fr', name: 'French', nativeName: 'Français' },
|
||||
{ dayjsImport: () => import('dayjs/locale/id.js'), code: 'id', name: 'Indonesian', nativeName: 'Indonesia' },
|
||||
{ dayjsImport: () => import('dayjs/locale/it.js'), code: 'it', name: 'Italian', nativeName: 'Italiano' },
|
||||
{ dayjsImport: () => import('dayjs/locale/pt.js'), code: 'pt', name: 'Portuguese', nativeName: 'Português' },
|
||||
{
|
||||
dayjsImport: () => import('dayjs/locale/pt.js'),
|
||||
code: 'pt-pt',
|
||||
name: 'Portuguese',
|
||||
nativeName: 'Português (Portugal)',
|
||||
},
|
||||
{
|
||||
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: '日本語' },
|
||||
{ code: 'ko', name: 'Korean', nativeName: '한국어' },
|
||||
{ code: 'zu', name: 'Zulu', nativeName: 'isiZulu' },
|
||||
{ code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' },
|
||||
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
|
||||
{ code: 'ro', name: 'Romanian', nativeName: 'Română' },
|
||||
{ code: 'bg', name: 'Bulgarian', nativeName: 'български' },
|
||||
{ code: 'cs', name: 'Czech', nativeName: 'čeština' },
|
||||
{ code: 'pl', name: 'Polish', nativeName: 'polski' },
|
||||
{ code: 'no', name: 'Norwegian', nativeName: 'Norsk' },
|
||||
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
|
||||
{ code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' },
|
||||
{ code: 'ms', name: 'Malay', nativeName: 'Malaysia' },
|
||||
{ code: 'mn', name: 'Mongolian', nativeName: 'Монгол' },
|
||||
{ code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
|
||||
{ code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' },
|
||||
{ code: 'hu', name: 'Hungarian', nativeName: 'Magyar' },
|
||||
{ code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' },
|
||||
{ code: 'he', name: 'Hebrew', nativeName: 'עברית' },
|
||||
{ code: 'fil', name: 'Filipino', nativeName: 'Filipino' },
|
||||
{ code: 'fi', name: 'Finnish', nativeName: 'suomi' },
|
||||
{ code: 'fa', name: 'Persian', nativeName: 'فارسی' },
|
||||
{ code: 'eu', name: 'Basque', nativeName: 'euskara' },
|
||||
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά' },
|
||||
{ code: 'da', name: 'Danish', nativeName: 'dansk' },
|
||||
{ code: 'bn', name: 'Bengali', nativeName: 'বাংলা' },
|
||||
{ code: 'lt', name: 'Lithuanian', nativeName: 'lietuvių kalba' },
|
||||
{ code: 'sh', name: 'Serbo-Croatian', nativeName: 'srpskohrvatski' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ja.js'), code: 'ja', name: 'Japanese', nativeName: '日本語' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ko.js'), code: 'ko', name: 'Korean', nativeName: '한국어' },
|
||||
{ dayjsImport: () => import('dayjs/locale/af.js'), code: 'zu', name: 'Zulu', nativeName: 'isiZulu' },
|
||||
{ dayjsImport: () => import('dayjs/locale/af.js'), code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' },
|
||||
{ dayjsImport: () => import('dayjs/locale/uk.js'), code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ro.js'), code: 'ro', name: 'Romanian', nativeName: 'Română' },
|
||||
{ dayjsImport: () => import('dayjs/locale/bg.js'), code: 'bg', name: 'Bulgarian', nativeName: 'български' },
|
||||
{ dayjsImport: () => import('dayjs/locale/cs.js'), code: 'cs', name: 'Czech', nativeName: 'čeština' },
|
||||
{ dayjsImport: () => import('dayjs/locale/pl.js'), code: 'pl', name: 'Polish', nativeName: 'polski' },
|
||||
{ dayjsImport: () => import('dayjs/locale/nb.js'), code: 'no', name: 'Norwegian', nativeName: 'Norsk' },
|
||||
{ dayjsImport: () => import('dayjs/locale/nl.js'), code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
|
||||
{ dayjsImport: () => import('dayjs/locale/my.js'), code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ms.js'), code: 'ms', name: 'Malay', nativeName: 'Malaysia' },
|
||||
{ dayjsImport: () => import('dayjs/locale/mn.js'), code: 'mn', name: 'Mongolian', nativeName: 'Монгол' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ml.js'), code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
|
||||
{ dayjsImport: () => import('dayjs/locale/ku.js'), code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' },
|
||||
{ dayjsImport: () => import('dayjs/locale/hu.js'), code: 'hu', name: 'Hungarian', nativeName: 'Magyar' },
|
||||
{ dayjsImport: () => import('dayjs/locale/hr.js'), code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' },
|
||||
{ dayjsImport: () => import('dayjs/locale/he.js'), code: 'he', name: 'Hebrew', nativeName: 'עברית' },
|
||||
{ dayjsImport: () => import('dayjs/locale/tl-ph'), code: 'fil', name: 'Filipino', nativeName: 'Filipino' },
|
||||
{ dayjsImport: () => import('dayjs/locale/fi.js'), code: 'fi', name: 'Finnish', nativeName: 'suomi' },
|
||||
{ dayjsImport: () => import('dayjs/locale/fa.js'), code: 'fa', name: 'Persian', nativeName: 'فارسی' },
|
||||
{ dayjsImport: () => import('dayjs/locale/eu.js'), code: 'eu', name: 'Basque', nativeName: 'euskara' },
|
||||
{ dayjsImport: () => import('dayjs/locale/el.js'), code: 'el', name: 'Greek', nativeName: 'Ελληνικά' },
|
||||
{ dayjsImport: () => import('dayjs/locale/da.js'), code: 'da', name: 'Danish', nativeName: 'dansk' },
|
||||
{ dayjsImport: () => import('dayjs/locale/bn.js'), code: 'bn', name: 'Bengali', nativeName: 'বাংলা' },
|
||||
{ dayjsImport: () => import('dayjs/locale/lt.js'), code: 'lt', name: 'Lithuanian', nativeName: 'lietuvių kalba' },
|
||||
{
|
||||
dayjsImport: loadDefaultDayJsLocale,
|
||||
code: 'sh',
|
||||
name: 'Serbo-Croatian',
|
||||
nativeName: 'srpskohrvatski',
|
||||
},
|
||||
|
||||
{ code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' },
|
||||
{ code: 'am', name: 'Amharic', nativeName: 'አማርኛ' },
|
||||
{ code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' },
|
||||
{ code: 'be', name: 'Belarusian', nativeName: 'беларуская' },
|
||||
{ code: 'bs', name: 'Bosnian', nativeName: 'bosanski' },
|
||||
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
||||
{ code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
||||
{ dayjsImport: () => import('dayjs/locale/af.js'), code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' },
|
||||
{ dayjsImport: () => import('dayjs/locale/am.js'), code: 'am', name: 'Amharic', nativeName: 'አማርኛ' },
|
||||
{ dayjsImport: () => import('dayjs/locale/az.js'), code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' },
|
||||
{ dayjsImport: () => import('dayjs/locale/be.js'), code: 'be', name: 'Belarusian', nativeName: 'беларуская' },
|
||||
{ dayjsImport: () => import('dayjs/locale/bs.js'), code: 'bs', name: 'Bosnian', nativeName: 'bosanski' },
|
||||
{ dayjsImport: () => import('dayjs/locale/sv.js'), code: 'sv', name: 'Swedish', nativeName: 'svenska' },
|
||||
];
|
||||
|
||||
export function langCodeToName(code: string): string {
|
||||
@@ -120,3 +141,20 @@ export const langSortCmp = (a: string, b: string) => {
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
41
yarn.lock
41
yarn.lock
@@ -1400,6 +1400,19 @@
|
||||
clsx "^2.0.0"
|
||||
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":
|
||||
version "5.14.16"
|
||||
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"
|
||||
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":
|
||||
version "2.1.5"
|
||||
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"
|
||||
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:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||
|
||||
Reference in New Issue
Block a user