mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
feat(ui): add a Date Format setting to choose language or browser dates
Since #6160 dates follow the selected UI language, which is wrong for users who run the UI in one language but want their own region's date format. The Personal page now has a Date Format select with "Match language" (default, current behavior) and "Match browser". The choice is kept in localStorage and read by useDateLocale, which returns no locale in browser mode so Intl uses the browser's own. Six screens imported react-admin's DateField directly and so ignored the language setting (playlist, radio and missing files lists, album table, song and album info). They now use the common DateField wrapper, which also reads the record from the record context.
This commit is contained in:
parent
95f67d2c4e
commit
4112b73ac7
13 changed files with 65 additions and 11 deletions
|
|
@ -8,7 +8,6 @@ import {
|
|||
ArrayField,
|
||||
BooleanField,
|
||||
ChipField,
|
||||
DateField,
|
||||
FunctionField,
|
||||
SingleFieldList,
|
||||
TextField,
|
||||
|
|
@ -17,6 +16,7 @@ import {
|
|||
} from 'react-admin'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
DateField,
|
||||
ArtistLinkField,
|
||||
MultiLineTextField,
|
||||
ParticipantsInfo,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
Datagrid,
|
||||
DatagridBody,
|
||||
DatagridRow,
|
||||
DateField,
|
||||
NumberField,
|
||||
TextField,
|
||||
FunctionField,
|
||||
|
|
@ -13,6 +12,7 @@ import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useDrag } from 'react-dnd'
|
||||
import {
|
||||
DateField,
|
||||
ArtistLinkField,
|
||||
ArtworkAvatar,
|
||||
DurationField,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react'
|
||||
import { isDateSet } from '../utils/validations'
|
||||
import { DateField as RADateField } from 'react-admin'
|
||||
import { DateField as RADateField, useRecordContext } from 'react-admin'
|
||||
import { useDateLocale } from '../i18n/useDateLocale'
|
||||
|
||||
export const DateField = (props) => {
|
||||
const { record, source } = props
|
||||
const record = useRecordContext(props)
|
||||
const locale = useDateLocale()
|
||||
const value = record?.[source]
|
||||
const value = record?.[props.source]
|
||||
if (!isDateSet(value)) return null
|
||||
return <RADateField locales={locale} {...props} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { RecordContextProvider } from 'react-admin'
|
||||
import { DateField } from './DateField'
|
||||
|
||||
vi.mock('react-admin', async (importOriginal) => ({
|
||||
|
|
@ -29,4 +30,13 @@ describe('<DateField>', () => {
|
|||
)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('reads the record from the record context', () => {
|
||||
render(
|
||||
<RecordContextProvider value={record}>
|
||||
<DateField source="updatedAt" />
|
||||
</RecordContextProvider>,
|
||||
)
|
||||
expect(screen.getByText('17.9.2026')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import TableContainer from '@material-ui/core/TableContainer'
|
|||
import TableRow from '@material-ui/core/TableRow'
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
TextField,
|
||||
NumberField,
|
||||
FunctionField,
|
||||
|
|
@ -15,6 +14,7 @@ import {
|
|||
} from 'react-admin'
|
||||
import { humanize, underscore } from 'inflection'
|
||||
import {
|
||||
DateField,
|
||||
ArtistLinkField,
|
||||
BitrateField,
|
||||
ParticipantsInfo,
|
||||
|
|
|
|||
|
|
@ -677,6 +677,11 @@
|
|||
"options": {
|
||||
"theme": "Theme",
|
||||
"language": "Language",
|
||||
"dateFormat": "Date Format",
|
||||
"dateFormats": {
|
||||
"language": "Match language",
|
||||
"browser": "Match browser"
|
||||
},
|
||||
"defaultView": "Default View",
|
||||
"desktop_notifications": "Desktop Notifications",
|
||||
"lastfmNotConfigured": "Last.fm API-Key is not configured",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { useLocale } from 'react-admin'
|
||||
|
||||
export const getStoredDateFormat = () =>
|
||||
localStorage.getItem('dateFormat') || 'language'
|
||||
|
||||
// Our language codes are mostly region-less ("en"), and Intl reads a bare "en"
|
||||
// as en-US. Borrow the region from the browser when it speaks the same language.
|
||||
const resolveDateLocale = (locale, browserLocales = []) => {
|
||||
|
|
@ -10,5 +13,10 @@ const resolveDateLocale = (locale, browserLocales = []) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const useDateLocale = () =>
|
||||
resolveDateLocale(useLocale(), navigator.languages)
|
||||
// With the "browser" date format, return undefined so Intl uses the browser's
|
||||
// own locale, regardless of the selected language.
|
||||
export const useDateLocale = () => {
|
||||
const locale = useLocale()
|
||||
if (getStoredDateFormat() === 'browser') return undefined
|
||||
return resolveDateLocale(locale, navigator.languages)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ vi.mock('react-admin', () => ({
|
|||
describe('useDateLocale', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.removeItem('dateFormat')
|
||||
})
|
||||
|
||||
const renderWith = async (locale, browserLocales) => {
|
||||
|
|
@ -42,4 +43,10 @@ describe('useDateLocale', () => {
|
|||
const result = await renderWith(undefined, ['en-GB'])
|
||||
expect(result.current).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined to use the browser locale when set to browser', async () => {
|
||||
localStorage.setItem('dateFormat', 'browser')
|
||||
const result = await renderWith('en', ['fr-FR'])
|
||||
expect(result.current).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
DateField,
|
||||
List,
|
||||
Pagination,
|
||||
SizeField,
|
||||
|
|
@ -7,7 +8,6 @@ import {
|
|||
} from '../common/index'
|
||||
import {
|
||||
Datagrid,
|
||||
DateField,
|
||||
TextField,
|
||||
downloadCSV,
|
||||
Filter,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { SimpleForm, Title, useTranslate } from 'react-admin'
|
|||
import { Card } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { SelectLanguage } from './SelectLanguage'
|
||||
import { SelectDateFormat } from './SelectDateFormat'
|
||||
import { SelectTheme } from './SelectTheme'
|
||||
import { SelectDefaultView } from './SelectDefaultView'
|
||||
import { NotificationsToggle } from './NotificationsToggle'
|
||||
|
|
@ -24,6 +25,7 @@ const Personal = () => {
|
|||
<SimpleForm toolbar={null} variant={'outlined'}>
|
||||
<SelectTheme />
|
||||
<SelectLanguage />
|
||||
<SelectDateFormat />
|
||||
<SelectDefaultView />
|
||||
{config.enableReplayGain && <ReplayGainToggle />}
|
||||
<NotificationsToggle />
|
||||
|
|
|
|||
22
ui/src/personal/SelectDateFormat.jsx
Normal file
22
ui/src/personal/SelectDateFormat.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { SelectInput, useTranslate } from 'react-admin'
|
||||
import { getStoredDateFormat } from '../i18n/useDateLocale'
|
||||
|
||||
export const SelectDateFormat = (props) => {
|
||||
const translate = useTranslate()
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
source="dateFormat"
|
||||
label={translate('menu.personal.options.dateFormat')}
|
||||
defaultValue={getStoredDateFormat()}
|
||||
choices={[
|
||||
{ id: 'language', name: 'menu.personal.options.dateFormats.language' },
|
||||
{ id: 'browser', name: 'menu.personal.options.dateFormats.browser' },
|
||||
]}
|
||||
onChange={(event) => {
|
||||
localStorage.setItem('dateFormat', event.target.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
Datagrid,
|
||||
DateField,
|
||||
EditButton,
|
||||
Filter,
|
||||
NullableBooleanInput,
|
||||
|
|
@ -20,6 +19,7 @@ import Switch from '@material-ui/core/Switch'
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import {
|
||||
DateField,
|
||||
ArtworkAvatar,
|
||||
DurationField,
|
||||
List,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React, { cloneElement } from 'react'
|
|||
import {
|
||||
CreateButton,
|
||||
Datagrid,
|
||||
DateField,
|
||||
EditButton,
|
||||
Filter,
|
||||
sanitizeListRestProps,
|
||||
|
|
@ -15,6 +14,7 @@ import {
|
|||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import {
|
||||
DateField,
|
||||
List,
|
||||
defaultRowsPerPageOptions,
|
||||
getStoredPerPage,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue