mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 11:28:47 +02:00
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:
@@ -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})
|
||||||
|
|||||||
@@ -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
28
frontend/src/lib/dates.ts
Normal 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`;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user