diff --git a/ui/src/album/AlbumDetails.jsx b/ui/src/album/AlbumDetails.jsx
index f55d94ffc..7fbc479db 100644
--- a/ui/src/album/AlbumDetails.jsx
+++ b/ui/src/album/AlbumDetails.jsx
@@ -31,6 +31,7 @@ import {
SizeField,
useAlbumsPerPage,
} from '../common'
+import { useDateLocale } from '../i18n/useDateLocale'
import { formatFullDate, intersperse } from '../utils'
import AlbumExternalLinks from './AlbumExternalLinks'
import { SafeHTML } from '../common/SafeHTML'
@@ -151,6 +152,7 @@ export const Details = (props) => {
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
const translate = useTranslate()
const record = useRecordContext(props)
+ const locale = useDateLocale()
// Create an array of detail elements
let details = []
@@ -161,12 +163,13 @@ export const Details = (props) => {
// Calculate date related fields
const yearRange = formatRange(record, 'year')
- const date = record.date ? formatFullDate(record.date) : yearRange
+ const date = record.date ? formatFullDate(record.date, locale) : yearRange
const originalDate = record.originalDate
- ? formatFullDate(record.originalDate)
+ ? formatFullDate(record.originalDate, locale)
: formatRange(record, 'originalYear')
- const releaseDate = record?.releaseDate && formatFullDate(record.releaseDate)
+ const releaseDate =
+ record?.releaseDate && formatFullDate(record.releaseDate, locale)
const dateToUse = originalDate || date
const isOriginalDate = originalDate && dateToUse !== date
diff --git a/ui/src/common/DateField.jsx b/ui/src/common/DateField.jsx
index dce24a2b9..dac9cff08 100644
--- a/ui/src/common/DateField.jsx
+++ b/ui/src/common/DateField.jsx
@@ -1,12 +1,14 @@
import React from 'react'
import { isDateSet } from '../utils/validations'
import { DateField as RADateField } from 'react-admin'
+import { useDateLocale } from '../i18n/useDateLocale'
export const DateField = (props) => {
const { record, source } = props
+ const locale = useDateLocale()
const value = record?.[source]
if (!isDateSet(value)) return null
- return
+ return
}
DateField.defaultProps = {
diff --git a/ui/src/common/DateField.test.jsx b/ui/src/common/DateField.test.jsx
new file mode 100644
index 000000000..47d56756b
--- /dev/null
+++ b/ui/src/common/DateField.test.jsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { DateField } from './DateField'
+
+vi.mock('react-admin', async (importOriginal) => ({
+ ...(await importOriginal()),
+ useLocale: vi.fn(),
+}))
+
+describe('', () => {
+ const record = { id: '1', updatedAt: '2026-09-17T14:30:00Z' }
+
+ beforeEach(async () => {
+ vi.clearAllMocks()
+ vi.spyOn(navigator, 'languages', 'get').mockReturnValue([])
+ const { useLocale } = await import('react-admin')
+ vi.mocked(useLocale).mockReturnValue('de')
+ })
+
+ it('formats the date using the selected language', () => {
+ render()
+ expect(screen.getByText('17.9.2026')).toBeInTheDocument()
+ })
+
+ it('renders nothing when the date is not set', () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toBeEmptyDOMElement()
+ })
+})
diff --git a/ui/src/common/LoveButton.jsx b/ui/src/common/LoveButton.jsx
index 492ba95b3..128b375c2 100644
--- a/ui/src/common/LoveButton.jsx
+++ b/ui/src/common/LoveButton.jsx
@@ -6,6 +6,8 @@ import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import clsx from 'clsx'
import { useToggleLove } from './useToggleLove'
+import { useDateLocale } from '../i18n/useDateLocale'
+import { formatDateTime } from '../utils/formatters'
import { useRecordContext } from 'react-admin'
import config from '../config'
import { isDateSet } from '../utils/validations'
@@ -40,6 +42,7 @@ export const LoveButton = ({
const record = useRecordContext({ record: recordProp }) || {}
const classes = useStyles({ color, visible, loved: record.starred })
const [toggleLove, loading] = useToggleLove(resource, record)
+ const locale = useDateLocale()
const handleToggleLove = useCallback(
(e) => {
@@ -61,7 +64,7 @@ export const LoveButton = ({
className={clsx(classes.love, className)}
title={
isDateSet(record.starredAt)
- ? new Date(record.starredAt).toLocaleString()
+ ? formatDateTime(record.starredAt, locale)
: undefined
}
{...rest}
diff --git a/ui/src/common/RatingField.jsx b/ui/src/common/RatingField.jsx
index f92b0d948..f892b475a 100644
--- a/ui/src/common/RatingField.jsx
+++ b/ui/src/common/RatingField.jsx
@@ -6,6 +6,8 @@ import { isDateSet } from '../utils/validations'
import StarBorderIcon from '@material-ui/icons/StarBorder'
import clsx from 'clsx'
import { useRating } from './useRating'
+import { useDateLocale } from '../i18n/useDateLocale'
+import { formatDateTime } from '../utils/formatters'
import { useRecordContext } from 'react-admin'
const useStyles = makeStyles({
@@ -32,6 +34,7 @@ export const RatingField = ({
const record = useRecordContext(rest) || {}
const [rate, rating] = useRating(resource, record)
const classes = useStyles({ color, visible })
+ const locale = useDateLocale()
const stopPropagation = (e) => {
e.stopPropagation()
@@ -50,7 +53,7 @@ export const RatingField = ({
onClick={(e) => stopPropagation(e)}
title={
isDateSet(record.ratedAt)
- ? new Date(record.ratedAt).toLocaleString()
+ ? formatDateTime(record.ratedAt, locale)
: undefined
}
>
diff --git a/ui/src/i18n/useDateLocale.js b/ui/src/i18n/useDateLocale.js
new file mode 100644
index 000000000..c9298a473
--- /dev/null
+++ b/ui/src/i18n/useDateLocale.js
@@ -0,0 +1,14 @@
+import { useLocale } from 'react-admin'
+
+// 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 = []) => {
+ if (!locale || locale.includes('-')) return locale
+ const base = locale.toLowerCase()
+ return (
+ browserLocales.find((l) => l.toLowerCase().split('-')[0] === base) || locale
+ )
+}
+
+export const useDateLocale = () =>
+ resolveDateLocale(useLocale(), navigator.languages)
diff --git a/ui/src/i18n/useDateLocale.test.js b/ui/src/i18n/useDateLocale.test.js
new file mode 100644
index 000000000..c0ea5ab40
--- /dev/null
+++ b/ui/src/i18n/useDateLocale.test.js
@@ -0,0 +1,45 @@
+import { renderHook } from '@testing-library/react-hooks'
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { useDateLocale } from './useDateLocale'
+
+vi.mock('react-admin', () => ({
+ useLocale: vi.fn(),
+}))
+
+describe('useDateLocale', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ const renderWith = async (locale, browserLocales) => {
+ const { useLocale } = await import('react-admin')
+ vi.mocked(useLocale).mockReturnValue(locale)
+ vi.spyOn(navigator, 'languages', 'get').mockReturnValue(browserLocales)
+ return renderHook(() => useDateLocale()).result
+ }
+
+ it('adds the region from the browser when the language has none', async () => {
+ const result = await renderWith('en', ['en-GB', 'fr-FR'])
+ expect(result.current).toEqual('en-GB')
+ })
+
+ it('falls back to the language when no browser entry matches', async () => {
+ const result = await renderWith('de', ['en-US', 'fr-FR'])
+ expect(result.current).toEqual('de')
+ })
+
+ it('keeps a language that already carries a region or script', async () => {
+ expect((await renderWith('pt-br', ['pt-PT'])).current).toEqual('pt-br')
+ expect((await renderWith('zh-Hans', ['zh-TW'])).current).toEqual('zh-Hans')
+ })
+
+ it('matches the browser language case-insensitively', async () => {
+ const result = await renderWith('pt', ['PT-PT'])
+ expect(result.current).toEqual('PT-PT')
+ })
+
+ it('returns undefined when there is no language', async () => {
+ const result = await renderWith(undefined, ['en-GB'])
+ expect(result.current).toBeUndefined()
+ })
+})
diff --git a/ui/src/user/UserList.jsx b/ui/src/user/UserList.jsx
index 4faf31785..7922ca08d 100644
--- a/ui/src/user/UserList.jsx
+++ b/ui/src/user/UserList.jsx
@@ -9,6 +9,8 @@ import {
} from 'react-admin'
import { useMediaQuery } from '@material-ui/core'
import { List, DateField } from '../common'
+import { useDateLocale } from '../i18n/useDateLocale'
+import { formatDateTime } from '../utils/formatters'
const UserFilter = (props) => (
@@ -18,6 +20,7 @@ const UserFilter = (props) => (
const UserList = (props) => {
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
+ const locale = useDateLocale()
return (
{
record.userName}
secondaryText={(record) =>
- record.lastLoginAt && new Date(record.lastLoginAt).toLocaleString()
+ record.lastLoginAt && formatDateTime(record.lastLoginAt, locale)
}
tertiaryText={(record) => (record.isAdmin ? '[admin]️' : '')}
/>
diff --git a/ui/src/utils/formatters.js b/ui/src/utils/formatters.js
index cfcb84b05..f7c59c195 100644
--- a/ui/src/utils/formatters.js
+++ b/ui/src/utils/formatters.js
@@ -95,6 +95,9 @@ export const formatFullDate = (date, locale) => {
return new Date(date).toLocaleDateString(locale, options)
}
+export const formatDateTime = (value, locale) =>
+ new Date(value).toLocaleString(locale)
+
export const formatNumber = (value, locale) => {
if (value === null || value === undefined) return '0'
return value.toLocaleString(locale)