@samitouri / QOSami-HFS / commits / 618a6315

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