Localization sweep: shared es-CR dates module, no more en-US strays

src/lib/dates.ts is the single source for month names and date
formatting; the three per-page month arrays are gone (with an
off-by-one guard — the shared array is 1-indexed). formatDate and the
Analytics chart tooltips now speak es-CR, Analytics headings are
Spanish, and the cycle selector says 'Todo el período' (UX-19, FE-17,
UX-07).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-10 17:59:16 -06:00
parent cdfa4ad90a
commit 8b7f3dff40
7 changed files with 42 additions and 20 deletions

View File

@@ -49,7 +49,7 @@ export default function BillingCycleSelector({ value, onChange }: Props) {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All time</SelectItem>
<SelectItem value="all">Todo el período</SelectItem>
{cycles.map((c) => (
<SelectItem key={`${c.year}-${c.month}`} value={`${c.year}-${c.month}`}>
{c.label} ({c.count})

View File

@@ -1,4 +1,5 @@
import ConfirmDialog from '@/components/ConfirmDialog';
import { MONTH_NAMES_ES_SHORT as MONTH_NAMES } from '@/lib/dates';
import { useState, useRef, useEffect } from 'react';
import { Pencil } from 'lucide-react';
@@ -15,10 +16,6 @@ import {
TableRow,
} from '@/components/ui/table';
const MONTH_NAMES = [
'', 'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun',
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic',
];
const FRESH_START_YEAR = 2026;
const FRESH_START_MONTH = 3;

28
frontend/src/lib/dates.ts Normal file
View File

@@ -0,0 +1,28 @@
/** Single source of truth for date display — the app speaks es-CR (UX-19). */
export const MONTH_NAMES_ES = [
'', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre',
];
export const MONTH_NAMES_ES_SHORT = [
'', 'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun',
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic',
];
/** "2 abr" */
export function formatShortDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('es-CR', {
month: 'short',
day: 'numeric',
});
}
/** "hace 3 días" / "hace 2 horas" / "hace un momento" */
export function formatRelativeAge(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
const hours = ms / 3_600_000;
if (hours < 1) return 'hace un momento';
if (hours < 48) return `hace ${Math.round(hours)} h`;
return `hace ${Math.round(hours / 24)} días`;
}

View File

@@ -1,3 +1,5 @@
import { formatShortDate } from './dates';
export function formatAmount(amount: number, currency: string) {
const abs = Math.abs(amount);
if (currency === 'BTC') return abs.toFixed(8);
@@ -17,5 +19,5 @@ export function formatLocalDatetime(d: Date): string {
}
export function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return formatShortDate(dateStr);
}

View File

@@ -65,7 +65,7 @@ const trendChartConfig = {
const dailyChartConfig = {
total: {
label: 'Daily Spending',
label: 'Gasto Diario',
color: 'var(--chart-2)',
},
} satisfies ChartConfig;
@@ -127,7 +127,7 @@ export default function Analytics() {
<BarChart3 className="w-5 h-5 text-primary" />
<h1 className="text-2xl font-bold font-heading">Analytics</h1>
</div>
<p className="text-sm text-muted-foreground mt-1">Spending breakdown and trends</p>
<p className="text-sm text-muted-foreground mt-1">Desglose y tendencias de gasto</p>
</div>
<BillingCycleSelector value={cycle} onChange={setCycle} />
</div>
@@ -150,7 +150,7 @@ export default function Analytics() {
<CardContent>
{byCategory.length === 0 ? (
<div className="h-64 flex items-center justify-center text-muted-foreground text-sm">
No data for this period
Sin datos para este período
</div>
) : (
<div className="flex flex-col items-center">
@@ -248,7 +248,7 @@ export default function Analytics() {
<CardContent>
{daily.length === 0 ? (
<div className="h-48 flex items-center justify-center text-muted-foreground text-sm">
No data for this period
Sin datos para este período
</div>
) : (
<ChartContainer data-sensitive config={dailyChartConfig} className="h-[240px] w-full">
@@ -272,7 +272,7 @@ export default function Analytics() {
<ChartTooltipContent
formatter={(value) => formatCRC(Number(value))}
labelFormatter={(label) =>
new Date(label).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
new Date(label).toLocaleDateString('es-CR', { month: 'short', day: 'numeric' })
}
/>
}

View File

@@ -4,6 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api, { type Transaction } from '@/lib/api';
import { MONTH_NAMES_ES as MONTH_NAMES } from '@/lib/dates';
import { useBudget } from '@/hooks/useBudget';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -12,10 +13,6 @@ import MonthlyDetail from '@/components/budget/MonthlyDetail';
import RecurringItemsManager from '@/components/budget/RecurringItemsManager';
import TransactionList from '@/components/TransactionList';
const MONTH_NAMES = [
'', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre',
];
const MIN_YEAR = 2026;
const MAX_YEAR = 2030;

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { MONTH_NAMES_ES_SHORT as MONTH_NAMES_ES } from '@/lib/dates';
import ErrorState from '@/components/ErrorState';
import {
BarChart,
@@ -53,10 +54,6 @@ const METER_COLORS: Record<string, string> = {
'9345': '#f59e0b',
};
const MONTH_NAMES_ES = [
'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun',
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic',
];
const DEFAULT_METER_COLOR = '#8b5cf6';
@@ -67,7 +64,8 @@ const formatCRC = (amount: number): string =>
function periodLabel(period: string): string {
const [yearStr, monthStr] = period.split('-');
const monthIdx = parseInt(monthStr, 10) - 1;
// The shared month array is 1-indexed ('' at index 0).
const monthIdx = parseInt(monthStr, 10);
return `${MONTH_NAMES_ES[monthIdx]} ${yearStr.slice(2)}`;
}