diff --git a/src/__tests__/date-validation.test.ts b/src/__tests__/date-validation.test.ts index a247b028..084252d8 100644 --- a/src/__tests__/date-validation.test.ts +++ b/src/__tests__/date-validation.test.ts @@ -2,8 +2,30 @@ import { describe, expect, it } from 'vitest'; import { apiDateString, dateStringForFormat } from '$lib/domain/shared'; import { maintenanceFormSchema } from '$lib/domain/maintenance'; import { complianceFormSchema } from '$lib/domain/compliance'; +import { formatDate, type FormatConfig } from '$lib/helper/date.helper'; + +const formatConfig: FormatConfig = { + dateFormat: 'd. MMMM yyyy', + timezone: 'UTC', + locale: 'en', + currency: 'USD', + unitOfVolume: 'liters', + unitOfLpg: 'liters', + unitOfCng: 'kg', + unitOfDistance: 'km', + mileageUnitFormat: 'km/L' +}; describe('localized date validation', () => { + it.each([ + ['cs', '3. září 2026'], + ['ru', '3. сентября 2026'] + ])('formats dates with %s month names', (locale, expected) => { + expect(formatDate(new Date('2026-09-03T12:00:00Z'), { ...formatConfig, locale })).toBe( + expected + ); + }); + it('accepts days greater than 12 in day-first formats', () => { expect(dateStringForFormat('dd/MM/yyyy').safeParse('13/08/2026').success).toBe(true); expect(dateStringForFormat('dd/MM/yyyy').safeParse('31/12/2026').success).toBe(true); diff --git a/src/lib/components/dashboard/StackedAreaChart.svelte b/src/lib/components/dashboard/StackedAreaChart.svelte index ca718971..6bcf3928 100644 --- a/src/lib/components/dashboard/StackedAreaChart.svelte +++ b/src/lib/components/dashboard/StackedAreaChart.svelte @@ -15,19 +15,22 @@ reports_type_maintenance, reports_type_compliance } from '$lib/paraglide/messages/_index.js'; - import { formatCurrency } from '$lib/helper/format.helper'; + import { formatCurrency, formatDate } from '$lib/helper/format.helper'; + import { getLocale } from '$lib/paraglide/runtime.js'; let { data, title, loading = false, - bare = false + bare = false, + xAxisFormatter = (value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' }) }: { data: MonthlyExpensePoint[]; title: string; loading?: boolean; /** Render content only — surrounding card chrome is provided by the parent. */ bare?: boolean; + xAxisFormatter?: (_: Date) => string; } = $props(); const SERIES = [ @@ -42,7 +45,7 @@ const chartProps = { xAxis: { - format: (v: Date) => v.toLocaleDateString('en-IN', { month: 'short', year: '2-digit' }) + format: (value: Date) => xAxisFormatter(value) }, yAxis: { format: (v: number) => formatCurrency(v) @@ -103,11 +106,7 @@ props={chartProps} > {#snippet tooltip()} - - v.toLocaleDateString('en-IN', { month: 'long', year: 'numeric' })} - indicator="dot" - > + {#snippet formatter({ value, name })} {name} diff --git a/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte b/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte index 234fe9a8..f237d046 100644 --- a/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte +++ b/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte @@ -4,6 +4,8 @@ import { calculateFuelAmountData } from '$stores/chart.svelte'; import { fuelLogStore } from '$stores/fuel-log.svelte'; import { vehicleStore } from '$stores/vehicle.svelte'; + import { getLocale } from '$lib/paraglide/runtime.js'; + import { formatDate } from '$lib/helper/format.helper'; let { loading }: { loading: boolean } = $props(); @@ -33,8 +35,8 @@ color="var(--chart-1)" loading={loading || fuelLogStore.processing} valueFormatter={(value: number) => `${value.toFixed(1)} L`} - xFormatter={(v: Date) => - v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + xFormatter={(value: Date) => formatDate(value)} + xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })} > {#snippet filter()} `${value.toFixed(1)} L`} - xFormatter={(v: Date) => - v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + xFormatter={(value: Date) => formatDate(value)} + xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })} > {#snippet filter()} formatMileage(value, series.fuelType)} - xFormatter={(v: Date) => - v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + xFormatter={(value: Date) => formatDate(value)} + xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })} > {#snippet filter()} import StackedAreaChart from '$dashboard/StackedAreaChart.svelte'; import type { DashboardSummary } from '$lib/domain/dashboard'; + import { getLocale } from '$lib/paraglide/runtime.js'; let { summary, loading }: { summary: DashboardSummary | null; loading: boolean } = $props(); @@ -10,4 +11,5 @@ data={summary?.expenses.monthlyTrend ?? []} title="Monthly Expense Trend" {loading} + xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })} /> diff --git a/src/lib/components/feature/overview/AreaChart.svelte b/src/lib/components/feature/overview/AreaChart.svelte index 09f2242b..96455699 100644 --- a/src/lib/components/feature/overview/AreaChart.svelte +++ b/src/lib/components/feature/overview/AreaChart.svelte @@ -20,6 +20,7 @@ label, title, xFormatter, + xAxisFormatter, valueFormatter, loading = false, bare = false, @@ -30,6 +31,7 @@ label: string; title: string; xFormatter: (_: Date) => string; + xAxisFormatter: (_: Date) => string; valueFormatter?: (_: number) => string; loading?: boolean; /** Render content only — surrounding card chrome is provided by the parent. */ @@ -46,7 +48,7 @@ motion: 'tween' }, xAxis: { - format: (v: Date) => v.toLocaleDateString('en-IN', { month: 'short' }) + format: (value: Date) => xAxisFormatter(value) }, yAxis: { format: (v: number) => valueFormatter?.(v) ?? v.toString() diff --git a/src/lib/components/feature/overview/VehicleTrendChart.svelte b/src/lib/components/feature/overview/VehicleTrendChart.svelte index 38187108..daf12310 100644 --- a/src/lib/components/feature/overview/VehicleTrendChart.svelte +++ b/src/lib/components/feature/overview/VehicleTrendChart.svelte @@ -17,6 +17,7 @@ series, title, xFormatter, + xAxisFormatter, valueFormatter, loading = false, bare = false, @@ -25,6 +26,7 @@ series: VehicleTrendSeries[]; title: string; xFormatter: (_: Date) => string; + xAxisFormatter: (_: Date) => string; valueFormatter?: (_: number, _series: VehicleTrendSeries) => string; loading?: boolean; /** Render content only — surrounding card chrome is provided by the parent. */ @@ -67,7 +69,7 @@ const chartProps = { xAxis: { - format: (v: Date) => v.toLocaleDateString('en-IN', { month: 'short' }) + format: (value: Date) => xAxisFormatter(value) }, yAxis: { format: (v: number) => diff --git a/src/lib/helper/date.helper.ts b/src/lib/helper/date.helper.ts index 2e40a465..e8a12b81 100644 --- a/src/lib/helper/date.helper.ts +++ b/src/lib/helper/date.helper.ts @@ -1,7 +1,7 @@ import type { DateValue } from '@internationalized/date'; import { format, parse } from 'date-fns'; import { formatInTimeZone, fromZonedTime } from 'date-fns-tz'; -import { es, fr, de, hi } from 'date-fns/locale'; +import { ar, cs, de, enUS, es, fi, fr, hi, hu, it, pt, ro, ru } from 'date-fns/locale'; export interface FormatConfig { dateFormat: string; @@ -17,15 +17,32 @@ export interface FormatConfig { const getDateFnsLocale = (locale: string) => { switch (locale) { + case 'ar': + return ar; + case 'cs': + return cs; + case 'de': + return de; case 'es': return es; + case 'fi': + return fi; case 'fr': return fr; - case 'de': - return de; case 'hi': return hi; + case 'hu': + return hu; + case 'it': + return it; + case 'pt-PT': + return pt; + case 'ro': + return ro; + case 'ru': + return ru; case 'en': + return enUS; default: return undefined; } diff --git a/src/routes/(app)/fuel/+page.svelte b/src/routes/(app)/fuel/+page.svelte index 67d158ff..81708838 100644 --- a/src/routes/(app)/fuel/+page.svelte +++ b/src/routes/(app)/fuel/+page.svelte @@ -7,8 +7,9 @@ import { fuelLogStore } from '$stores/fuel-log.svelte'; import { chartStore } from '$stores/chart.svelte'; import { ACCENT } from '$lib/helper/accent-color.helper'; - import { formatCurrency, formatMileage } from '$lib/helper/format.helper'; + import { formatCurrency, formatDate, formatMileage } from '$lib/helper/format.helper'; import VehicleTrendChart from '$feature/overview/VehicleTrendChart.svelte'; + import { getLocale } from '$lib/paraglide/runtime.js'; import FuelLogTab from '$feature/fuel/FuelLogTab.svelte'; import { Features } from '$lib/helper/feature.helper'; import FeaturePageShell from '$feature/shared/FeaturePageShell.svelte'; @@ -77,16 +78,21 @@ title={widget_fuel_consumption_trend()} loading={fuelLogStore.processing} valueFormatter={(value) => `${value.toFixed(1)} L`} - xFormatter={(v: Date) => - v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + xFormatter={(value: Date) => formatDate(value)} + xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })} /> formatMileage(value, series.fuelType)} - xFormatter={(v: Date) => - v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + xFormatter={(value: Date) => + value.toLocaleDateString(getLocale(), { + day: '2-digit', + month: 'short', + year: 'numeric' + })} + xAxisFormatter={(value: Date) => value.toLocaleDateString(getLocale(), { month: 'short' })} />