better code
Massimo Melina committed
Apr 19, 2026 at 17:37 UTC
f188ba3c40923b6e7fb1329f2defffdfaa0cff53
4 files changed
+34
-163
admin/src/App.ts
+15
-2
@@ -15,7 +15,7 @@ import ConfigFilePage from './ConfigFilePage'
15
import { useSnapState } from './state'
16
import { useEventListener } from 'usehooks-ts'
17
import { AriaOnly, isMac, useFixSticky, xlate } from './misc'
18
-import { getLocale } from './locale'
18
+import { loadLocale } from './locale'
19
import { fillFlexParentSx } from './DataTable'
20
21
// always use useMemo with setTitleSide
@@ -24,7 +24,7 @@ export interface PageProps { setTitleSide: (content: ReactNode, fullWidth?: bool
24
function App() {
25
return h(ThemeProvider, { theme: useMyTheme() },
26
h(ApplyTheme, {},
27
- h(LocalizationProvider, { dateAdapter: AdapterDayjs, adapterLocale: getLocale() },
27
+ h(Localization, {},
28
h(LoginRequired, {},
29
h(HashRouter, {},
30
h(Dialogs, {
@@ -36,6 +36,19 @@ function App() {
36
}, h(Routed) ))) )))
37
}
38
39
+function Localization(props: any) {
40
+ const [locale, setLocale] = useState('')
41
+ useEffect(() => {
42
+ let cancelled = false
43
+ loadLocale().then(locale => {
44
+ if (!cancelled)
45
+ setLocale(locale || '')
46
+ })
47
+ return () => { cancelled = true }
48
+ }, [])
49
+ return h(LocalizationProvider, { dateAdapter: AdapterDayjs, adapterLocale: locale, ...props })
50
+}
51
+
52
function ApplyTheme(props:any) {
53
return h(Box, {
54
sx: { bgcolor: 'background.default', color: 'text.primary', flex: 1,
admin/src/ArrayField.ts
+2
-2
@@ -1,5 +1,5 @@
1
import { createElement as h, Fragment, isValidElement, useMemo, useState } from 'react'
2
-import { callable, Dict, Functionable, isOrderedEqual, setHidden, swap } from './misc'
2
+import { callable, Dict, formatTimestamp, Functionable, isOrderedEqual, setHidden, swap } from './misc'
3
import { Add, Edit, Delete, ArrowUpward, ArrowDownward, Undo, Check } from '@mui/icons-material'
4
import { DialogOptions, FormDialog, formDialog } from './dialog'
5
import { GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
@@ -212,7 +212,7 @@ const byType: Dict<{ field?: Partial<FieldDescriptor>, column?: Partial<GridColD
212
field: { comp: DateTimeField },
213
column: {
214
minWidth: 96, flex: 0.5,
215
- renderCell: ({ value }) => value && new Date(value).toLocaleString(),
215
+ renderCell: ({ value }) => formatTimestamp(value),
216
}
217
}
218
}
admin/src/OptionsPage.ts
+1
-1
@@ -54,7 +54,7 @@ export default function OptionsPage() {
54
const isLH = hn === 'localhost'
55
const isV6 = hn.includes(':')
56
const listenInterfaceOptions = [
57
- { label: "any", value: '' },
57
+ { label: "any", value: '', disabled: false },
58
{ label: "any IPv4", value: '0.0.0.0', disabled: !isLH && isV6 },
59
{ label: "any IPv6", value: '::', disabled: !isLH && !isV6 },
60
...['127.0.0.1', '::1'].map(x => ({ label: x, value: x, disabled: !isLH && hn !== x })),
admin/src/locale.ts
+16
-158
@@ -1,165 +1,15 @@
1
-import 'dayjs/locale/af'
2
-import 'dayjs/locale/bi'
3
-import 'dayjs/locale/de-ch'
4
-import 'dayjs/locale/en'
5
-import 'dayjs/locale/fr-ch'
6
-import 'dayjs/locale/hy-am'
7
-import 'dayjs/locale/ky'
8
-import 'dayjs/locale/mt'
9
-import 'dayjs/locale/ro'
10
-import 'dayjs/locale/sv'
11
-import 'dayjs/locale/tzm'
12
-import 'dayjs/locale/am'
13
-import 'dayjs/locale/bm'
14
-import 'dayjs/locale/de'
15
-import 'dayjs/locale/eo'
16
-import 'dayjs/locale/fr'
17
-import 'dayjs/locale/id'
18
-import 'dayjs/locale/lb'
19
-import 'dayjs/locale/my'
20
-import 'dayjs/locale/ru'
21
-import 'dayjs/locale/sw'
22
-import 'dayjs/locale/ug-cn'
23
-import 'dayjs/locale/ar-dz'
24
-import 'dayjs/locale/bn-bd'
25
-import 'dayjs/locale/dv'
26
-import 'dayjs/locale/es-do'
27
-import 'dayjs/locale/fy'
28
-import 'dayjs/locale/is'
29
-import 'dayjs/locale/lo'
30
-import 'dayjs/locale/nb'
31
-import 'dayjs/locale/rw'
32
-import 'dayjs/locale/ta'
33
-import 'dayjs/locale/uk'
34
-import 'dayjs/locale/ar-iq'
35
-import 'dayjs/locale/bn'
36
-import 'dayjs/locale/el'
37
-import 'dayjs/locale/es-mx'
38
-import 'dayjs/locale/ga'
39
-import 'dayjs/locale/it-ch'
40
-import 'dayjs/locale/lt'
41
-import 'dayjs/locale/ne'
42
-import 'dayjs/locale/sd'
43
-import 'dayjs/locale/te'
44
-import 'dayjs/locale/ur'
45
-import 'dayjs/locale/ar-kw'
46
-import 'dayjs/locale/bo'
47
-import 'dayjs/locale/en-au'
48
-import 'dayjs/locale/es-pr'
49
-import 'dayjs/locale/gd'
50
-import 'dayjs/locale/it'
51
-import 'dayjs/locale/lv'
52
-import 'dayjs/locale/nl-be'
53
-import 'dayjs/locale/se'
54
-import 'dayjs/locale/tet'
55
-import 'dayjs/locale/uz-latn'
56
-import 'dayjs/locale/ar-ly'
57
-import 'dayjs/locale/br'
58
-import 'dayjs/locale/en-ca'
59
-import 'dayjs/locale/es-us'
60
-import 'dayjs/locale/gl'
61
-import 'dayjs/locale/ja'
62
-import 'dayjs/locale/me'
63
-import 'dayjs/locale/nl'
64
-import 'dayjs/locale/si'
65
-import 'dayjs/locale/tg'
66
-import 'dayjs/locale/uz'
67
-import 'dayjs/locale/ar-ma'
68
-import 'dayjs/locale/bs'
69
-import 'dayjs/locale/en-gb'
70
-import 'dayjs/locale/es'
71
-import 'dayjs/locale/gom-latn'
72
-import 'dayjs/locale/jv'
73
-import 'dayjs/locale/mi'
74
-import 'dayjs/locale/nn'
75
-import 'dayjs/locale/sk'
76
-import 'dayjs/locale/th'
77
-import 'dayjs/locale/vi'
78
-import 'dayjs/locale/ar-sa'
79
-import 'dayjs/locale/ca'
80
-import 'dayjs/locale/en-ie'
81
-import 'dayjs/locale/et'
82
-import 'dayjs/locale/gu'
83
-import 'dayjs/locale/ka'
84
-import 'dayjs/locale/mk'
85
-import 'dayjs/locale/oc-lnc'
86
-import 'dayjs/locale/sl'
87
-import 'dayjs/locale/tk'
88
-import 'dayjs/locale/x-pseudo'
89
-import 'dayjs/locale/ar-tn'
90
-import 'dayjs/locale/cs'
91
-import 'dayjs/locale/en-il'
92
-import 'dayjs/locale/eu'
93
-import 'dayjs/locale/he'
94
-import 'dayjs/locale/kk'
95
-import 'dayjs/locale/ml'
96
-import 'dayjs/locale/pa-in'
97
-import 'dayjs/locale/sq'
98
-import 'dayjs/locale/tl-ph'
99
-import 'dayjs/locale/yo'
100
-import 'dayjs/locale/ar'
101
-import 'dayjs/locale/cv'
102
-import 'dayjs/locale/en-in'
103
-import 'dayjs/locale/fa'
104
-import 'dayjs/locale/hi'
105
-import 'dayjs/locale/km'
106
-import 'dayjs/locale/mn'
107
-import 'dayjs/locale/pl'
108
-import 'dayjs/locale/sr-cyrl'
109
-import 'dayjs/locale/tlh'
110
-import 'dayjs/locale/zh-cn'
111
-import 'dayjs/locale/az'
112
-import 'dayjs/locale/cy'
113
-import 'dayjs/locale/en-nz'
114
-import 'dayjs/locale/fi'
115
-import 'dayjs/locale/hr'
116
-import 'dayjs/locale/kn'
117
-import 'dayjs/locale/mr'
118
-import 'dayjs/locale/pt-br'
119
-import 'dayjs/locale/sr'
120
-import 'dayjs/locale/tr'
121
-import 'dayjs/locale/zh-hk'
122
-import 'dayjs/locale/be'
123
-import 'dayjs/locale/da'
124
-import 'dayjs/locale/en-sg'
125
-import 'dayjs/locale/fo'
126
-import 'dayjs/locale/ht'
127
-import 'dayjs/locale/ko'
128
-import 'dayjs/locale/ms-my'
129
-import 'dayjs/locale/pt'
130
-import 'dayjs/locale/ss'
131
-import 'dayjs/locale/tzl'
132
-import 'dayjs/locale/zh-tw'
133
-import 'dayjs/locale/bg'
134
-import 'dayjs/locale/de-at'
135
-import 'dayjs/locale/en-tt'
136
-import 'dayjs/locale/fr-ca'
137
-import 'dayjs/locale/hu'
138
-import 'dayjs/locale/ku'
139
-import 'dayjs/locale/ms'
140
-import 'dayjs/locale/rn'
141
-import 'dayjs/locale/sv-fi'
142
-import 'dayjs/locale/tzm-latn'
143
-import 'dayjs/locale/zh'
1
+/// <reference types="vite/client" />
2
import { findDefined } from './misc'
3
146
-const dayjs_locales = ['af', 'ar', 'ar-dz', 'ar-kw', 'ar-ly', 'ar-ma', 'ar-sa', 'ar-tn', 'az', 'be', 'bg', 'bm', 'bn',
147
- 'bo', 'br', 'bs', 'ca', 'cs', 'cv', 'cy', 'da', 'de', 'de-at', 'de-ch', 'dv', 'el', 'en', 'en-au', 'en-ca', 'en-gb',
148
- 'en-ie', 'en-il', 'en-nz', 'en-SG', 'eo', 'es', 'es-do', 'es-us', 'et', 'eu', 'fa', 'fi', 'fo', 'fr', 'fr-ca',
149
- 'fr-ch', 'fy', 'ga', 'gd', 'gl', 'gom-latn', 'gu', 'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'is', 'it', 'it-ch',
150
- 'ja', 'jv', 'ka', 'kk', 'km', 'kn', 'ko', 'ku', 'ky', 'lb', 'lo', 'lt', 'lv', 'me', 'mi', 'mk', 'ml', 'mn', 'mr',
151
- 'ms', 'ms-my', 'mt', 'my', 'nb', 'ne', 'nl', 'nl-be', 'nn', 'oc-lnc', 'pa-in', 'pl', 'pt', 'pt-br', 'ro', 'ru',
152
- 'sd', 'se', 'si', 'sk', 'sl', 'sq', 'sr', 'sr-cyrl', 'ss', 'sv', 'sw', 'ta', 'te', 'tet', 'tg', 'th', 'tl-ph',
153
- 'tlh', 'tr', 'tzl', 'tzm', 'tzm-latn', 'ug-cn', 'uk', 'ur', 'uz', 'uz-latn', 'vi', 'x-pseudo', 'yo', 'zh-cn',
154
- 'zh-hk', 'zh-tw']
4
+const localeLoaders = import.meta.glob('../../node_modules/dayjs/locale/*.js')
5
+const localePaths = new Map(Object.keys(localeLoaders).map(path => [path.match(/([^/]+)\.js$/)![1], path]))
6
+const localeAliases: Record<string, string> = { zn: 'zh-cn', no: 'nb' }
7
8
function lang2locale(lang: string): string | undefined {
157
- return dayjs_locales.includes(lang) && lang
158
- || {
159
- zn: 'zh-cn',
160
- no: 'nb',
161
- }[lang]
162
- || lang.includes('-') && lang2locale(lang.split('-')[0])
9
+ const normalized = lang.toLowerCase()
10
+ return localePaths.has(normalized) && normalized
11
+ || localeAliases[normalized]
12
+ || normalized.includes('-') && lang2locale(normalized.split('-')[0])
13
|| undefined
14
}
15
@@ -167,3 +17,11 @@ export function getLocale() {
17
return findDefined([navigator.language, ...navigator.languages], lang2locale)
18
}
19
20
+export async function loadLocale() {
21
+ const locale = getLocale()
22
+ if (!locale)
23
+ return
24
+ // AdapterDayjs needs the matching dayjs locale module registered before adapterLocale can use it.
25
+ await localeLoaders[localePaths.get(locale)!]?.()
26
+ return locale
27
+}