fix: timestamp in files list was always browser-dependant instead of following force_lang
Massimo Melina committed
Nov 20, 2024 at 11:33 UTC
618a63158c4f6bc4865acb0c1a350e2a2b4e44b3
6 files changed
+26
-11
admin/src/LangPage.ts
+2
-2
@@ -5,7 +5,7 @@ import { apiCall, useApiEx, useApiList } from './api'
5
import { DataTable } from './DataTable';
6
import { Alert, Box } from '@mui/material'
7
import { Delete, Upload } from '@mui/icons-material'
8
-import { readFile, selectFiles } from './misc'
8
+import { CFG, readFile, selectFiles } from './misc'
9
import { Btn, IconBtn } from './mui'
10
import { PageProps } from './App'
11
import _ from 'lodash'
@@ -89,7 +89,7 @@ export default function LangPage({ setTitleSide }: PageProps) {
89
}
90
91
function ForceLang({ langs }: { langs: string[] }) {
92
- const K = 'force_lang'
92
+ const K = CFG.force_lang
93
const { data, reload, loading } = useApiEx('get_config', { only: [K] })
94
const [lang, setLang] = useState()
95
useEffect(() => setLang(data?.[K] ?? lang), [data])
frontend/src/BrowseFiles.ts
+5
-3
@@ -4,7 +4,9 @@ import { Link, useNavigate } from 'react-router-dom'
4
import { createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState,
5
useId} from 'react'
6
import { useEventListener, useMediaQuery, useWindowSize } from 'usehooks-ts'
7
-import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, isCtrlKey, hfsEvent } from './misc'
7
+import {
8
+ domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, isCtrlKey, hfsEvent, formatTimestamp
9
+} from './misc'
10
import { Checkbox, CustomCode, iconBtn, Spinner } from './components'
11
import { Head } from './Head'
12
import { DirEntry, state, useSnapState } from './state'
@@ -328,9 +330,9 @@ export const EntryDetails = memo(({ entry, midnight }: { entry: DirEntry, midnig
330
'aria-hidden': true,
331
onClick() { // mobile has no hover
332
if (shortTs)
331
- void alertDialog(t`Full timestamp:` + "\n" + time.toLocaleString())
333
+ void alertDialog(t`Full timestamp:` + "\n" + formatTimestamp(time))
334
}
333
- }, time.toLocaleString(navigator.language, {
335
+ }, formatTimestamp(time, {
336
...!shortTs || !today ? { year: shortTs ? dd : 'numeric', month: dd, day: dd } : null,
337
...!shortTs || today ? { hour: dd, minute: dd } : null,
338
})),
frontend/src/i18n.ts
+5
-1
@@ -8,7 +8,7 @@ import { watch } from 'valtio/utils'
8
const translations = getHFS().lang || {} // all dictionaries
9
const state = proxy({
10
embedded: '',
11
- langs: Object.keys(translations)
11
+ langs: getLangs()
12
})
13
const searchLangs: string[] = []
14
watch(get => {
@@ -18,6 +18,10 @@ watch(get => {
18
19
const warns = new Set() // avoid duplicates
20
21
+export function getLangs() {
22
+ return Object.keys(translations)
23
+}
24
+
25
// the hook ensures translation is refreshed when language changes
26
export function useI18N() {
27
useSnapshot(state)
frontend/src/misc.ts
+11
-2
@@ -4,12 +4,12 @@ import React, { createElement as h } from 'react'
4
import { iconBtn, Spinner } from './components'
5
import { newDialog, toast } from './dialog'
6
import { Icon } from './icons'
7
-import { Callback, Dict, domOn, getHFS, Html, HTTP_MESSAGES, useBatch } from '@hfs/shared'
7
+import { Callback, Dict, domOn, getHFS, getOrSet, Html, HTTP_MESSAGES, urlParams, useBatch } from '@hfs/shared'
8
import * as cross from '../../src/cross'
9
import * as shared from '@hfs/shared'
10
import { apiCall, getNotifications, useApi } from '@hfs/shared/api'
11
import { DirEntry, state, useSnapState } from './state'
12
-import { t } from './i18n'
12
+import { getLangs, t } from './i18n'
13
import * as dialogLib from './dialog'
14
import _ from 'lodash'
15
import { reloadList } from './useFetchList'
@@ -63,6 +63,15 @@ export function hfsEvent(name: string, params?:Dict) {
63
})
64
}
65
66
+export function formatTimestamp(x: number | string | Date, options?: Intl.DateTimeFormatOptions) {
67
+ const cached = getOrSet(formatTimestamp as any, 'langs', () => {
68
+ const ret = getLangs()
69
+ const def = urlParams.lang || navigator.language
70
+ return !ret.length || def.startsWith(ret[0]) ? def : ret // eg: if i'm ar-EG, and first translation is ar, keep ar-EG
71
+ })
72
+ return !x ? '' : (x instanceof Date ? x : new Date(x)).toLocaleString(cached, options)
73
+}
74
+
75
Object.assign(getHFS(), {
76
h, React, state, t, _, dialogLib, apiCall, useApi, reloadList, logout, Icon, hIcon, iconBtn, useBatch, fileShow,
77
toast, domOn, getNotifications, debounceAsync, useSnapState, DirEntry,
src/cross.ts
+1
-1
@@ -27,7 +27,7 @@ export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
27
export const CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknown', 'dynamic_dns_url',
28
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua', 'log_spam', 'track_ips',
29
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address', 'split_uploads',
30
- 'allow_session_ip_change'])
30
+ 'allow_session_ip_change', 'force_lang'])
31
export const LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' }
32
export type Dict<T=any> = Record<string, T>
33
export type Falsy = false | null | undefined | '' | 0
src/lang.ts
+2
-2
@@ -1,5 +1,5 @@
1
import Koa from 'koa'
2
-import { hasProp, onlyTruthy, tryJson, wantArray } from './misc'
2
+import { CFG, hasProp, onlyTruthy, tryJson, wantArray } from './misc'
3
import { readFile } from 'fs/promises'
4
import { defineConfig } from './config'
5
import { watchLoad } from './watchLoad'
@@ -57,7 +57,7 @@ export async function getLangData(ctx: Koa.Context) {
57
58
let forceLangData: any
59
let undo: any
60
-defineConfig('force_lang', '', v => {
60
+defineConfig(CFG.force_lang, '', v => {
61
undo?.()
62
if (!v)
63
return forceLangData = undefined