From 4112b73ac76c6f9b63ad6f36ef0eee6a3f62c86d Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 3 Oct 2026 13:02:19 -0400 Subject: [PATCH 1/2] 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. --- ui/src/album/AlbumInfo.jsx | 2 +- ui/src/album/AlbumTableView.jsx | 2 +- ui/src/common/DateField.jsx | 6 +++--- ui/src/common/DateField.test.jsx | 10 ++++++++++ ui/src/common/SongInfo.jsx | 2 +- ui/src/i18n/en.json | 5 +++++ ui/src/i18n/useDateLocale.js | 12 ++++++++++-- ui/src/i18n/useDateLocale.test.js | 7 +++++++ ui/src/missing/MissingFilesList.jsx | 2 +- ui/src/personal/Personal.jsx | 2 ++ ui/src/personal/SelectDateFormat.jsx | 22 ++++++++++++++++++++++ ui/src/playlist/PlaylistList.jsx | 2 +- ui/src/radio/RadioList.jsx | 2 +- 13 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 ui/src/personal/SelectDateFormat.jsx diff --git a/ui/src/album/AlbumInfo.jsx b/ui/src/album/AlbumInfo.jsx index 075841e43..1de904031 100644 --- a/ui/src/album/AlbumInfo.jsx +++ b/ui/src/album/AlbumInfo.jsx @@ -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, diff --git a/ui/src/album/AlbumTableView.jsx b/ui/src/album/AlbumTableView.jsx index 051d5988e..46a86c6bc 100644 --- a/ui/src/album/AlbumTableView.jsx +++ b/ui/src/album/AlbumTableView.jsx @@ -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, diff --git a/ui/src/common/DateField.jsx b/ui/src/common/DateField.jsx index dac9cff08..8aab34ae1 100644 --- a/ui/src/common/DateField.jsx +++ b/ui/src/common/DateField.jsx @@ -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 } diff --git a/ui/src/common/DateField.test.jsx b/ui/src/common/DateField.test.jsx index 47d56756b..7494b7887 100644 --- a/ui/src/common/DateField.test.jsx +++ b/ui/src/common/DateField.test.jsx @@ -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('', () => { ) expect(container).toBeEmptyDOMElement() }) + + it('reads the record from the record context', () => { + render( + + + , + ) + expect(screen.getByText('17.9.2026')).toBeInTheDocument() + }) }) diff --git a/ui/src/common/SongInfo.jsx b/ui/src/common/SongInfo.jsx index 1b1a014f1..b9f7bea8c 100644 --- a/ui/src/common/SongInfo.jsx +++ b/ui/src/common/SongInfo.jsx @@ -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, diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index f694ea75f..8f086f516 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -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", diff --git a/ui/src/i18n/useDateLocale.js b/ui/src/i18n/useDateLocale.js index c9298a473..f02402f29 100644 --- a/ui/src/i18n/useDateLocale.js +++ b/ui/src/i18n/useDateLocale.js @@ -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) +} diff --git a/ui/src/i18n/useDateLocale.test.js b/ui/src/i18n/useDateLocale.test.js index c0ea5ab40..16707a084 100644 --- a/ui/src/i18n/useDateLocale.test.js +++ b/ui/src/i18n/useDateLocale.test.js @@ -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() + }) }) diff --git a/ui/src/missing/MissingFilesList.jsx b/ui/src/missing/MissingFilesList.jsx index a09f552d9..64ab0487d 100644 --- a/ui/src/missing/MissingFilesList.jsx +++ b/ui/src/missing/MissingFilesList.jsx @@ -1,4 +1,5 @@ import { + DateField, List, Pagination, SizeField, @@ -7,7 +8,6 @@ import { } from '../common/index' import { Datagrid, - DateField, TextField, downloadCSV, Filter, diff --git a/ui/src/personal/Personal.jsx b/ui/src/personal/Personal.jsx index 84f9b63e6..75c4dd45a 100644 --- a/ui/src/personal/Personal.jsx +++ b/ui/src/personal/Personal.jsx @@ -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 = () => { + {config.enableReplayGain && } diff --git a/ui/src/personal/SelectDateFormat.jsx b/ui/src/personal/SelectDateFormat.jsx new file mode 100644 index 000000000..fdc9583d2 --- /dev/null +++ b/ui/src/personal/SelectDateFormat.jsx @@ -0,0 +1,22 @@ +import { SelectInput, useTranslate } from 'react-admin' +import { getStoredDateFormat } from '../i18n/useDateLocale' + +export const SelectDateFormat = (props) => { + const translate = useTranslate() + + return ( + { + localStorage.setItem('dateFormat', event.target.value) + }} + /> + ) +} diff --git a/ui/src/playlist/PlaylistList.jsx b/ui/src/playlist/PlaylistList.jsx index 14d819a4e..56f0bf532 100644 --- a/ui/src/playlist/PlaylistList.jsx +++ b/ui/src/playlist/PlaylistList.jsx @@ -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, diff --git a/ui/src/radio/RadioList.jsx b/ui/src/radio/RadioList.jsx index 0cfb2cd68..46e59ffcc 100644 --- a/ui/src/radio/RadioList.jsx +++ b/ui/src/radio/RadioList.jsx @@ -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, From feb33554072d63b2f8f3b96413abf238d1aebc92 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 3 Oct 2026 14:04:16 -0400 Subject: [PATCH 2/2] feat(ui): add pt-BR translations for the Date Format setting --- resources/i18n/pt-br.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json index 238919259..1a0c1ab8a 100644 --- a/resources/i18n/pt-br.json +++ b/resources/i18n/pt-br.json @@ -660,6 +660,11 @@ "options": { "theme": "Tema", "language": "Língua", + "dateFormat": "Formato de data", + "dateFormats": { + "language": "Igual à língua", + "browser": "Igual ao navegador" + }, "defaultView": "Tela inicial", "desktop_notifications": "Notificações", "lastfmScrobbling": "Enviar scrobbles para Last.fm",