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:
Deluan 2026-10-03 13:02:19 -04:00
commit 4112b73ac7
13 changed files with 65 additions and 11 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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} />
}

View file

@ -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()
})
})

View file

@ -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,

View file

@ -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",

View file

@ -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)
}

View file

@ -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()
})
})

View file

@ -1,4 +1,5 @@
import {
DateField,
List,
Pagination,
SizeField,
@ -7,7 +8,6 @@ import {
} from '../common/index'
import {
Datagrid,
DateField,
TextField,
downloadCSV,
Filter,

View file

@ -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 />

View 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)
}}
/>
)
}

View file

@ -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,

View file

@ -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,