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 /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All time</SelectItem> <SelectItem value="all">Todo el período</SelectItem>
{cycles.map((c) => ( {cycles.map((c) => (
<SelectItem key={`${c.year}-${c.month}`} value={`${c.year}-${c.month}`}> <SelectItem key={`${c.year}-${c.month}`} value={`${c.year}-${c.month}`}>
{c.label} ({c.count}) {c.label} ({c.count})

View File

@@ -1,4 +1,5 @@
import ConfirmDialog from '@/components/ConfirmDialog'; import ConfirmDialog from '@/components/ConfirmDialog';
import { MONTH_NAMES_ES_SHORT as MONTH_NAMES } from '@/lib/dates';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Pencil } from 'lucide-react'; import { Pencil } from 'lucide-react';
@@ -15,10 +16,6 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } 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_YEAR = 2026;
const FRESH_START_MONTH = 3; 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) { export function formatAmount(amount: number, currency: string) {
const abs = Math.abs(amount); const abs = Math.abs(amount);
if (currency === 'BTC') return abs.toFixed(8); if (currency === 'BTC') return abs.toFixed(8);
@@ -17,5 +19,5 @@ export function formatLocalDatetime(d: Date): string {
} }
export function formatDate(dateStr: 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 = { const dailyChartConfig = {
total: { total: {
label: 'Daily Spending', label: 'Gasto Diario',
color: 'var(--chart-2)', color: 'var(--chart-2)',
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
@@ -127,7 +127,7 @@ export default function Analytics() {
<BarChart3 className="w-5 h-5 text-primary" /> <BarChart3 className="w-5 h-5 text-primary" />
<h1 className="text-2xl font-bold font-heading">Analytics</h1> <h1 className="text-2xl font-bold font-heading">Analytics</h1>
</div> </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> </div>
<BillingCycleSelector value={cycle} onChange={setCycle} /> <BillingCycleSelector value={cycle} onChange={setCycle} />
</div> </div>
@@ -150,7 +150,7 @@ export default function Analytics() {
<CardContent> <CardContent>
{byCategory.length === 0 ? ( {byCategory.length === 0 ? (
<div className="h-64 flex items-center justify-center text-muted-foreground text-sm"> <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>
) : ( ) : (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@@ -248,7 +248,7 @@ export default function Analytics() {
<CardContent> <CardContent>
{daily.length === 0 ? ( {daily.length === 0 ? (
<div className="h-48 flex items-center justify-center text-muted-foreground text-sm"> <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> </div>
) : ( ) : (
<ChartContainer data-sensitive config={dailyChartConfig} className="h-[240px] w-full"> <ChartContainer data-sensitive config={dailyChartConfig} className="h-[240px] w-full">
@@ -272,7 +272,7 @@ export default function Analytics() {
<ChartTooltipContent <ChartTooltipContent
formatter={(value) => formatCRC(Number(value))} formatter={(value) => formatCRC(Number(value))}
labelFormatter={(label) => 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 { toast } from 'sonner';
import api, { type Transaction } from '@/lib/api'; import api, { type Transaction } from '@/lib/api';
import { MONTH_NAMES_ES as MONTH_NAMES } from '@/lib/dates';
import { useBudget } from '@/hooks/useBudget'; import { useBudget } from '@/hooks/useBudget';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 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 RecurringItemsManager from '@/components/budget/RecurringItemsManager';
import TransactionList from '@/components/TransactionList'; 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 MIN_YEAR = 2026;
const MAX_YEAR = 2030; const MAX_YEAR = 2030;

View File

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