Compare commits

...

2 commits

Author SHA1 Message Date
Deluan
feb3355407 feat(ui): add pt-BR translations for the Date Format setting 2026-10-03 14:04:16 -04:00
Deluan
4112b73ac7 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.
2026-10-03 13:02:19 -04:00
14 changed files with 70 additions and 11 deletions

View file

@ -660,6 +660,11 @@
"options": { "options": {
"theme": "Tema", "theme": "Tema",
"language": "Língua", "language": "Língua",
"dateFormat": "Formato de data",
"dateFormats": {
"language": "Igual à língua",
"browser": "Igual ao navegador"
},
"defaultView": "Tela inicial", "defaultView": "Tela inicial",
"desktop_notifications": "Notificações", "desktop_notifications": "Notificações",
"lastfmScrobbling": "Enviar scrobbles para Last.fm", "lastfmScrobbling": "Enviar scrobbles para Last.fm",

View file

@ -8,7 +8,6 @@ import {
ArrayField, ArrayField,
BooleanField, BooleanField,
ChipField, ChipField,
DateField,
FunctionField, FunctionField,
SingleFieldList, SingleFieldList,
TextField, TextField,
@ -17,6 +16,7 @@ import {
} from 'react-admin' } from 'react-admin'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { import {
DateField,
ArtistLinkField, ArtistLinkField,
MultiLineTextField, MultiLineTextField,
ParticipantsInfo, ParticipantsInfo,

View file

@ -3,7 +3,6 @@ import {
Datagrid, Datagrid,
DatagridBody, DatagridBody,
DatagridRow, DatagridRow,
DateField,
NumberField, NumberField,
TextField, TextField,
FunctionField, FunctionField,
@ -13,6 +12,7 @@ import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { useDrag } from 'react-dnd' import { useDrag } from 'react-dnd'
import { import {
DateField,
ArtistLinkField, ArtistLinkField,
ArtworkAvatar, ArtworkAvatar,
DurationField, DurationField,

View file

@ -1,12 +1,12 @@
import React from 'react' import React from 'react'
import { isDateSet } from '../utils/validations' 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' import { useDateLocale } from '../i18n/useDateLocale'
export const DateField = (props) => { export const DateField = (props) => {
const { record, source } = props const record = useRecordContext(props)
const locale = useDateLocale() const locale = useDateLocale()
const value = record?.[source] const value = record?.[props.source]
if (!isDateSet(value)) return null if (!isDateSet(value)) return null
return <RADateField locales={locale} {...props} /> return <RADateField locales={locale} {...props} />
} }

View file

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest' import { describe, it, expect, beforeEach, vi } from 'vitest'
import { RecordContextProvider } from 'react-admin'
import { DateField } from './DateField' import { DateField } from './DateField'
vi.mock('react-admin', async (importOriginal) => ({ vi.mock('react-admin', async (importOriginal) => ({
@ -29,4 +30,13 @@ describe('<DateField>', () => {
) )
expect(container).toBeEmptyDOMElement() 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 TableRow from '@material-ui/core/TableRow'
import { import {
BooleanField, BooleanField,
DateField,
TextField, TextField,
NumberField, NumberField,
FunctionField, FunctionField,
@ -15,6 +14,7 @@ import {
} from 'react-admin' } from 'react-admin'
import { humanize, underscore } from 'inflection' import { humanize, underscore } from 'inflection'
import { import {
DateField,
ArtistLinkField, ArtistLinkField,
BitrateField, BitrateField,
ParticipantsInfo, ParticipantsInfo,

View file

@ -677,6 +677,11 @@
"options": { "options": {
"theme": "Theme", "theme": "Theme",
"language": "Language", "language": "Language",
"dateFormat": "Date Format",
"dateFormats": {
"language": "Match language",
"browser": "Match browser"
},
"defaultView": "Default View", "defaultView": "Default View",
"desktop_notifications": "Desktop Notifications", "desktop_notifications": "Desktop Notifications",
"lastfmNotConfigured": "Last.fm API-Key is not configured", "lastfmNotConfigured": "Last.fm API-Key is not configured",

View file

@ -1,5 +1,8 @@
import { useLocale } from 'react-admin' 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" // 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. // as en-US. Borrow the region from the browser when it speaks the same language.
const resolveDateLocale = (locale, browserLocales = []) => { const resolveDateLocale = (locale, browserLocales = []) => {
@ -10,5 +13,10 @@ const resolveDateLocale = (locale, browserLocales = []) => {
) )
} }
export const useDateLocale = () => // With the "browser" date format, return undefined so Intl uses the browser's
resolveDateLocale(useLocale(), navigator.languages) // 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', () => { describe('useDateLocale', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
localStorage.removeItem('dateFormat')
}) })
const renderWith = async (locale, browserLocales) => { const renderWith = async (locale, browserLocales) => {
@ -42,4 +43,10 @@ describe('useDateLocale', () => {
const result = await renderWith(undefined, ['en-GB']) const result = await renderWith(undefined, ['en-GB'])
expect(result.current).toBeUndefined() 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 { import {
DateField,
List, List,
Pagination, Pagination,
SizeField, SizeField,
@ -7,7 +8,6 @@ import {
} from '../common/index' } from '../common/index'
import { import {
Datagrid, Datagrid,
DateField,
TextField, TextField,
downloadCSV, downloadCSV,
Filter, Filter,

View file

@ -2,6 +2,7 @@ import { SimpleForm, Title, useTranslate } from 'react-admin'
import { Card } from '@material-ui/core' import { Card } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { SelectLanguage } from './SelectLanguage' import { SelectLanguage } from './SelectLanguage'
import { SelectDateFormat } from './SelectDateFormat'
import { SelectTheme } from './SelectTheme' import { SelectTheme } from './SelectTheme'
import { SelectDefaultView } from './SelectDefaultView' import { SelectDefaultView } from './SelectDefaultView'
import { NotificationsToggle } from './NotificationsToggle' import { NotificationsToggle } from './NotificationsToggle'
@ -24,6 +25,7 @@ const Personal = () => {
<SimpleForm toolbar={null} variant={'outlined'}> <SimpleForm toolbar={null} variant={'outlined'}>
<SelectTheme /> <SelectTheme />
<SelectLanguage /> <SelectLanguage />
<SelectDateFormat />
<SelectDefaultView /> <SelectDefaultView />
{config.enableReplayGain && <ReplayGainToggle />} {config.enableReplayGain && <ReplayGainToggle />}
<NotificationsToggle /> <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 React, { useMemo } from 'react'
import { import {
Datagrid, Datagrid,
DateField,
EditButton, EditButton,
Filter, Filter,
NullableBooleanInput, NullableBooleanInput,
@ -20,6 +19,7 @@ import Switch from '@material-ui/core/Switch'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { useMediaQuery } from '@material-ui/core' import { useMediaQuery } from '@material-ui/core'
import { import {
DateField,
ArtworkAvatar, ArtworkAvatar,
DurationField, DurationField,
List, List,

View file

@ -3,7 +3,6 @@ import React, { cloneElement } from 'react'
import { import {
CreateButton, CreateButton,
Datagrid, Datagrid,
DateField,
EditButton, EditButton,
Filter, Filter,
sanitizeListRestProps, sanitizeListRestProps,
@ -15,6 +14,7 @@ import {
useTranslate, useTranslate,
} from 'react-admin' } from 'react-admin'
import { import {
DateField,
List, List,
defaultRowsPerPageOptions, defaultRowsPerPageOptions,
getStoredPerPage, getStoredPerPage,