diff --git a/app/components/device-detail/device-detail-box.tsx b/app/components/device-detail/device-detail-box.tsx index f4a90404..fd575f83 100644 --- a/app/components/device-detail/device-detail-box.tsx +++ b/app/components/device-detail/device-detail-box.tsx @@ -1,6 +1,4 @@ import clsx from 'clsx' -import { formatDistanceToNow } from 'date-fns' -import { de, enUS } from 'date-fns/locale' import { ChevronUp, Minus, @@ -81,6 +79,7 @@ import { useGlobalCompareMode } from './useGlobalCompareMode' import { type SensorWithLatestMeasurement } from '~/db/schema' import { getArchiveLink } from '~/lib/archive-link' import { type loader } from '~/routes/explore.$deviceId' +import { dateDiffToNowInWords } from '~/lib/date' export interface MeasurementProps { sensorId: string @@ -96,7 +95,6 @@ export default function DeviceDetailBox() { const matches = useMatches() const { toast } = useToast() const { t, i18n } = useTranslation('device-detail-box') - const dateLocale = i18n.language.startsWith('de') ? de : enUS const dateFormatter = new Intl.DateTimeFormat(i18n.resolvedLanguage, { year: 'numeric', month: 'long', @@ -569,15 +567,12 @@ export default function DeviceDetailBox() { >

{sensor.lastMeasurement - ? formatDistanceToNow( + ? dateDiffToNowInWords( + i18n.language, new Date( sensor.lastMeasurement .createdAt, ), - { - addSuffix: true, - locale: dateLocale, - }, ) : t('no_recent_data')}

@@ -656,15 +651,12 @@ export default function DeviceDetailBox() { >

{sensor.lastMeasurement - ? formatDistanceToNow( + ? dateDiffToNowInWords( + i18n.language, new Date( sensor.lastMeasurement .createdAt, ), - { - addSuffix: true, - locale: dateLocale, - }, ) : t('no_recent_data')}

diff --git a/app/components/device-detail/graph.tsx b/app/components/device-detail/graph.tsx index 2186a98f..5df5829e 100644 --- a/app/components/device-detail/graph.tsx +++ b/app/components/device-detail/graph.tsx @@ -11,7 +11,6 @@ import { type ChartOptions, } from 'chart.js' import 'chartjs-adapter-date-fns' -// import { de, enGB } from "date-fns/locale"; import { Download, RefreshCcw, X } from 'lucide-react' import { useMemo, diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index 6aa8caf4..ecc2d832 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -1,6 +1,5 @@ import bbox from '@turf/bbox' import { point, featureCollection } from '@turf/helpers' -import { format } from 'date-fns' import { type FeatureCollection, type Point } from 'geojson' import { CalendarClock } from 'lucide-react' import { useState, useEffect, useMemo, useCallback } from 'react' @@ -10,6 +9,7 @@ import { type LocationPoint, categorizeIntoTrips, } from '~/lib/mobile-box-helper' +import { useTranslation } from 'react-i18next' const FIT_PADDING = 100 @@ -121,6 +121,12 @@ export default function MobileOverviewLayer({ }: { locations: LocationPoint[] }) { + const { i18n } = useTranslation() + const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, { + hour: 'numeric', + minute: '2-digit', + }) + // Generate trips and assign colors once const trips = useMemo(() => categorizeIntoTrips(locations, 50), [locations]) @@ -451,7 +457,7 @@ export default function MobileOverviewLayer({ )}

- {format(new Date(popupInfo.startTime), 'Pp')} + {dateTimeFormat.format(new Date(popupInfo.startTime))}

{popupInfo.isCluster && @@ -461,7 +467,7 @@ export default function MobileOverviewLayer({ To

- {format(new Date(popupInfo.endTime), 'Pp')} + {dateTimeFormat.format(new Date(popupInfo.endTime))}

)} diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index f3978eb5..3eafa5f5 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -32,7 +32,7 @@ export function getColumns( useTranslation: UseTranslationResponse<'data-table', any>, opts?: { isOwner?: boolean }, ): ColumnDef[] { - const { t } = useTranslation + const { t, i18n } = useTranslation const isOwner = opts?.isOwner ?? false return [ { @@ -90,7 +90,7 @@ export function getColumns( }, cell: ({ row }) => { const date = new Date(row.getValue('createdAt')) - return
{date.toLocaleDateString()}
+ return
{date.toLocaleDateString(i18n.language)}
}, }, { diff --git a/app/components/ui/range-picker.tsx b/app/components/ui/range-picker.tsx index a7f9d939..d1b9a325 100644 --- a/app/components/ui/range-picker.tsx +++ b/app/components/ui/range-picker.tsx @@ -1,7 +1,6 @@ 'use client' import * as React from 'react' -import { addDays, format } from 'date-fns' import { CalendarIcon } from 'lucide-react' import { type DateRange } from '@daypicker/react' @@ -13,11 +12,19 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover' +import { useTranslation } from 'react-i18next' export function DatePickerWithRange() { + const { i18n } = useTranslation() const [date, setDate] = React.useState({ from: new Date(new Date().getFullYear(), 0, 20), - to: addDays(new Date(new Date().getFullYear(), 0, 20), 20), + to: new Date(new Date().getFullYear(), 1, 10), + }) + + const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, { + year: 'numeric', + month: 'long', + day: 'numeric', }) return ( @@ -34,11 +41,11 @@ export function DatePickerWithRange() { {date?.from ? ( date.to ? ( <> - {format(date.from, 'LLL dd, y')} -{' '} - {format(date.to, 'LLL dd, y')} + {dateTimeFormat.format(date.from)} -{' '} + {dateTimeFormat.format(date.to)} ) : ( - format(date.from, 'LLL dd, y') + dateTimeFormat.format(date.from) ) ) : ( Pick a date diff --git a/app/lib/date.ts b/app/lib/date.ts new file mode 100644 index 00000000..f61536f7 --- /dev/null +++ b/app/lib/date.ts @@ -0,0 +1,30 @@ +const ONE_MINUTE_IN_S = 60 +const ONE_HOUR_IN_S = 60 * ONE_MINUTE_IN_S +const ONE_DAY_IN_S = 24 * ONE_HOUR_IN_S +const ONE_WEEK_IN_S = 7 * ONE_DAY_IN_S +const ONE_MONTH_IN_S = 4 * ONE_WEEK_IN_S +const ONE_QUARTER_IN_S = 3 * ONE_MONTH_IN_S +const ONE_YEAR_IN_S = 12 * ONE_MONTH_IN_S + +export const dateDiffToNowInWords = (locale: string, date: Date) => { + const r = new Intl.RelativeTimeFormat(locale) + const now = new Date() + const diffInSeconds = Math.round((now.getTime() - date.getTime()) / 1000) + const absDiffInSeconds = Math.abs(diffInSeconds) + + if (absDiffInSeconds < ONE_MINUTE_IN_S) + return r.format(-diffInSeconds, 'second') + if (absDiffInSeconds < ONE_HOUR_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_MINUTE_IN_S), 'minute') + if (absDiffInSeconds < ONE_DAY_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_HOUR_IN_S), 'hour') + if (absDiffInSeconds < ONE_WEEK_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_DAY_IN_S), 'day') + if (absDiffInSeconds < ONE_MONTH_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_WEEK_IN_S), 'week') + if (absDiffInSeconds < ONE_QUARTER_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_MONTH_IN_S), 'month') + if (absDiffInSeconds < ONE_YEAR_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'quarter') + return r.format(-Math.round(diffInSeconds / ONE_YEAR_IN_S), 'year') +} diff --git a/app/routes/admin.devices._index.tsx b/app/routes/admin.devices._index.tsx index 64d2e9ba..47ed551b 100644 --- a/app/routes/admin.devices._index.tsx +++ b/app/routes/admin.devices._index.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router' import { type Route } from './+types/admin.devices._index' import { getDevices } from '~/db/models/device.server' +import { useTranslation } from 'react-i18next' export async function loader({}: Route.LoaderArgs) { const devices = await getDevices('json') @@ -11,6 +12,7 @@ export default function AdminDevicesIndexRoute({ loaderData, }: Route.ComponentProps) { const { devices } = loaderData + const { i18n } = useTranslation() return (
@@ -43,7 +45,7 @@ export default function AdminDevicesIndexRoute({ {device.status} - {new Date(device.createdAt).toLocaleString()} + {new Date(device.createdAt).toLocaleString(i18n.language)} @@ -45,10 +47,10 @@ export default function AdminUsersIndexRoute({ {user.role} - {new Date(user.createdAt).toLocaleString()} + {new Date(user.createdAt).toLocaleString(i18n.language)} - {new Date(user.updatedAt).toLocaleString()} + {new Date(user.updatedAt).toLocaleString(i18n.language)} {/* {user.devicesCount} diff --git a/app/routes/device.$deviceId.edit.logs.tsx b/app/routes/device.$deviceId.edit.logs.tsx index 03a32f13..720c76cd 100644 --- a/app/routes/device.$deviceId.edit.logs.tsx +++ b/app/routes/device.$deviceId.edit.logs.tsx @@ -94,7 +94,7 @@ export default function Logs() { const { logEntries } = useLoaderData() const actionData = useActionData() const { toast } = useToast() - const { t } = useTranslation('edit-device-logs') + const { t, i18n } = useTranslation('edit-device-logs') const [newLogContent, setNewLogContent] = useState('') const submit = useSubmit() @@ -169,7 +169,9 @@ export default function Logs() { {logEntry.content} - {new Date(logEntry.createdAt).toLocaleString()} + {new Date(logEntry.createdAt).toLocaleString( + i18n.language, + )}
diff --git a/app/routes/device.$deviceId.edit.transfer.tsx b/app/routes/device.$deviceId.edit.transfer.tsx index 82e57c3f..8566397f 100644 --- a/app/routes/device.$deviceId.edit.transfer.tsx +++ b/app/routes/device.$deviceId.edit.transfer.tsx @@ -144,7 +144,7 @@ export default function EditDeviceTransfer() { const { deviceName, existingTransfer } = useLoaderData() const actionData = useActionData() const navigation = useNavigation() - const { t } = useTranslation('device-transfer') + const { t, i18n } = useTranslation('device-transfer') const [copied, setCopied] = useState(false) @@ -293,7 +293,7 @@ export default function EditDeviceTransfer() {

{t('valid_until')}{' '} - {new Date(transferExpiresAt).toLocaleString(t('locale'), { + {new Date(transferExpiresAt).toLocaleString(i18n.language, { dateStyle: 'medium', timeStyle: 'short', })} diff --git a/app/routes/profile.$username.tsx b/app/routes/profile.$username.tsx index 64613a36..7a06919d 100644 --- a/app/routes/profile.$username.tsx +++ b/app/routes/profile.$username.tsx @@ -127,7 +127,7 @@ export default function ProfilePage() { deviceSchemas, } = useLoaderData() - const { t } = useTranslation('profile') + const { t, i18n } = useTranslation('profile') const columnsTranslation = useTranslation('data-table') const isOwner = !!profile?.userId && requestingUserId === profile.userId @@ -159,7 +159,7 @@ export default function ProfilePage() {

{t('user_since')}{' '} {new Date(profile?.user?.createdAt || '').toLocaleDateString( - t('locale'), + i18n.language, )}