i18n(es): batch 3 part 2 — locale-aware dates & relative times

- New src/lib/dateFnsI18n.ts re-exports date-fns with formatDistanceToNow /
  formatDistanceToNowStrict / formatDistance / format defaulting to the active
  locale, plus dateLocaleCode() for native toLocale{Date,Time}String calls.
- LanguageProvider keeps the locale in sync during render, so the first paint is
  already in-language.
- Codemod: 11 date-fns imports now use the shim; ~39 native date/time calls pass
  the app locale code instead of undefined/[]/hardcoded 'en-US'. Number
  .toLocaleString() calls are deliberately untouched.
This commit is contained in:
SkateSpots 2026-10-02 15:19:38 +07:00
parent 3e6354995a
commit 9357386bbd
45 changed files with 168 additions and 65 deletions

View file

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { Heart, Repeat2, Zap, AtSign, MessageCircle, CalendarCheck, Megaphone } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useAuthor } from '@/hooks/useAuthor';

View file

@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { Lock, Send, Loader2, ShieldCheck, MessageSquare, EyeOff } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo } from 'react';
import { Lock } from 'lucide-react';
import { parseEscrowToken, type EscrowTokenInfo } from '@/lib/cashuEscrow';
@ -13,7 +14,7 @@ function hostOf(url: string): string {
}
function formatDate(seconds: number): string {
return new Date(seconds * 1000).toLocaleDateString(undefined, {
return new Date(seconds * 1000).toLocaleDateString(dateLocaleCode(), {
year: 'numeric',
month: 'short',
day: 'numeric',

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
@ -54,7 +55,7 @@ interface EventCardProps {
function formatDate(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString('en-US', {
return date.toLocaleDateString(dateLocaleCode(), {
weekday: 'short',
month: 'short',
day: 'numeric',
@ -63,7 +64,7 @@ function formatDate(timestamp: number): string {
function formatTime(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toLocaleTimeString('en-US', {
return date.toLocaleTimeString(dateLocaleCode(), {
hour: 'numeric',
minute: '2-digit',
});
@ -99,7 +100,7 @@ export function EventCard({ event, compact = false, goingCount: goingCountProp }
<div className="flex-shrink-0 w-16 text-center">
<div className="bg-[#ccff00]/10 border-2 border-[#ccff00]/50 rounded-lg p-2">
<div className="text-xs font-bold text-[#ccff00] uppercase">
{new Date(event.start * 1000).toLocaleDateString('en-US', { month: 'short' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })}
</div>
<div className="text-2xl font-black text-white">
{new Date(event.start * 1000).getDate()}
@ -169,7 +170,7 @@ export function EventCard({ event, compact = false, goingCount: goingCountProp }
<div className="absolute top-3 left-3 flex items-center gap-2">
<div className="bg-background/95 backdrop-blur-sm rounded-lg px-3 py-2 text-center shadow-lg">
<div className="text-xs font-medium text-primary uppercase">
{new Date(event.start * 1000).toLocaleDateString('en-US', { month: 'short' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })}
</div>
<div className="text-xl font-bold">
{new Date(event.start * 1000).getDate()}

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { Megaphone, Loader2, Send, CalendarClock, MapPin, XCircle, Info } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo, useState, useRef, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Route, MapPin, Clock, Users } from 'lucide-react';
@ -132,11 +133,11 @@ function itemOnDay(item: CalendarItem, day: Date): boolean {
}
function formatTime(unix: number): string {
return new Date(unix * 1000).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
return new Date(unix * 1000).toLocaleTimeString(dateLocaleCode(), { hour: 'numeric', minute: '2-digit' });
}
function formatDayLong(d: Date): string {
return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
return d.toLocaleDateString(dateLocaleCode(), { weekday: 'long', month: 'long', day: 'numeric' });
}
// ─── Component ────────────────────────────────────────────────────────────────
@ -376,7 +377,7 @@ export function EventsCalendar({ events, tours, goingCounts, show, anchorUnix }:
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{multiDay
? `${new Date(it.start * 1000).toLocaleDateString([], { month: 'short', day: 'numeric' })} – ${new Date((it.end as number) * 1000).toLocaleDateString([], { month: 'short', day: 'numeric' })}`
? `${new Date(it.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric' })} – ${new Date((it.end as number) * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric' })}`
: it.allDay
? t('placesUi.calendar.allDay')
: formatTime(it.start)}

View file

@ -21,7 +21,7 @@ import { MessageCircle, MapPin, Search, CheckCircle, ShieldCheck, AlertTriangle,
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { ReportDialog } from '@/components/ReportDialog';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { useTranslation } from '@/contexts/language';
interface GearSwapItemCardProps {

View file

@ -8,6 +8,7 @@
* AI proxy both honour it.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState } from 'react';
import { nip19 } from 'nostr-tools';
import { useNostr } from '@nostrify/react';
@ -87,7 +88,7 @@ export function GrantProCard() {
title: t('settingsUi.grantPro.grantedTitle'),
description: t('settingsUi.grantPro.grantedDesc', {
npub: `${nip19.npubEncode(pubkey).slice(0, 14)}…`,
date: new Date(expiry * 1000).toLocaleDateString(),
date: new Date(expiry * 1000).toLocaleDateString(dateLocaleCode()),
}),
});
setTarget('');

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, Users, Pencil, Megaphone, Plus, CalendarClock } from 'lucide-react';
@ -11,7 +12,7 @@ import { cn } from '@/lib/utils';
import type { SkateEvent } from '@/lib/types';
function formatWhen(ts: number): string {
return new Date(ts * 1000).toLocaleDateString(undefined, {
return new Date(ts * 1000).toLocaleDateString(dateLocaleCode(), {
weekday: 'short',
month: 'short',
day: 'numeric',
@ -32,7 +33,7 @@ function HostedEventRow({ event, isPast }: { event: SkateEvent; isPast: boolean
<div className="flex-shrink-0 w-14 text-center">
<div className="rounded-lg border-2 border-[#ccff00]/40 bg-[#ccff00]/10 p-1.5">
<div className="text-[10px] font-black uppercase text-[#ccff00]">
{new Date(event.start * 1000).toLocaleDateString(undefined, { month: 'short' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })}
</div>
<div className="text-xl font-black text-white leading-none">
{new Date(event.start * 1000).getDate()}

View file

@ -28,7 +28,7 @@ import { getLastLocation } from '@/lib/lastLocation';
import { itemDistanceKm } from '@/lib/sortOptions';
import { TrustBadge } from '@/components/TrustBadge';
import { Clock, Search, Tag, Flag, MoreVertical, ShieldCheck, Calendar, Package, AlertTriangle, CheckCircle, RotateCcw, Trash2, Loader2 } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import type { MarketplaceItem } from '@/lib/types';
import { GEAR_CONDITIONS, GEAR_CATEGORIES, PRICE_TYPES, LISTING_REPORT_REASONS, formatPrice } from '@/lib/types';
import { useTranslation } from '@/contexts/language';

View file

@ -39,7 +39,7 @@ import {
Heart,
Zap,
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { useTranslation } from '@/contexts/language';
// Kind constants

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo } from 'react';
import { ImageWithFallback } from '@/components/ImageWithFallback';
import { Link } from 'react-router-dom';
@ -152,7 +153,7 @@ export function NewsletterIssueView({ pubkey, identifier }: { pubkey: string; id
const authorName = author.data?.metadata?.name ?? genUserName(pubkey);
const authorPic = author.data?.metadata?.picture;
const date = new Date(issue.publishedAt * 1000).toLocaleDateString('en-US', {
const date = new Date(issue.publishedAt * 1000).toLocaleDateString(dateLocaleCode(), {
year: 'numeric',
month: 'long',
day: 'numeric',

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
@ -148,7 +149,7 @@ function NewsletterItem({
pubkey: issue.pubkey,
identifier: issue.identifier,
});
const date = new Date(issue.publishedAt * 1000).toLocaleDateString(undefined, {
const date = new Date(issue.publishedAt * 1000).toLocaleDateString(dateLocaleCode(), {
month: 'short',
day: 'numeric',
});

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import L from 'leaflet';
import { Loader2, MapPin, Navigation, AlertTriangle, LocateFixed, Play, Square, Crosshair, Volume2, VolumeX, Check, Search } from 'lucide-react';
@ -117,7 +118,7 @@ function speedValue(distanceM: number, durationS: number): string | undefined {
/** Local clock time you'd arrive if you set off now. */
function etaClock(durationS: number): string {
return new Date(Date.now() + durationS * 1000).toLocaleTimeString([], {
return new Date(Date.now() + durationS * 1000).toLocaleTimeString(dateLocaleCode(), {
hour: '2-digit',
minute: '2-digit',
});

View file

@ -7,6 +7,7 @@
* Falls back to static FAQ if AI is unavailable.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Input } from '@/components/ui/input';
@ -1656,7 +1657,7 @@ Instructions:
{isPro ? (
<span
className="flex items-center gap-0.5 rounded bg-[#ccff00]/20 px-1 py-0.5 text-[8px] font-black uppercase tracking-wider text-[#ccff00]"
title={expiresAt ? t('sasquatchUi.activeUntil', { date: new Date(expiresAt * 1000).toLocaleDateString() }) : t('sasquatchUi.platformOwner')}
title={expiresAt ? t('sasquatchUi.activeUntil', { date: new Date(expiresAt * 1000).toLocaleDateString(dateLocaleCode()) }) : t('sasquatchUi.platformOwner')}
>
⚡ Pro{proDaysLeft !== null ? ` · ${proDaysLeft}d` : ''}
</span>

View file

@ -6,6 +6,7 @@
* no-ads, community-funded app — Pro is the thank-you.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useEffect, useState } from 'react';
import QRCode from 'qrcode';
import {
@ -112,7 +113,7 @@ export function SasquatchProDialog({
<p className="font-black text-[#ccff00]">{t('sasquatchUi.proSupporter')}</p>
{expiresAt && (
<p className="mt-1 text-xs text-gray-300">
{t('sasquatchUi.activeUntil', { date: new Date(expiresAt * 1000).toLocaleDateString() })}
{t('sasquatchUi.activeUntil', { date: new Date(expiresAt * 1000).toLocaleDateString(dateLocaleCode()) })}
</p>
)}
</div>

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { formatDistanceToNowStrict } from 'date-fns';
import { formatDistanceToNowStrict } from '@/lib/dateFnsI18n';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { MessageCircle, Send, Radio, Info, Globe } from 'lucide-react';

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useEffect, useRef, useMemo, useCallback } from 'react';
import L from 'leaflet';
import 'leaflet.markercluster';
@ -621,7 +622,7 @@ function createPopupContent(
const dayDiv = document.createElement('div');
dayDiv.className = 'p-1';
const date = new Date(day.date);
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
const dayName = date.toLocaleDateString(dateLocaleCode(), { weekday: 'short' });
dayDiv.innerHTML = `
<div class="font-semibold text-gray-700">${escapeHtml(dayName)}</div>
<div class="text-lg my-1">${escapeHtml(day.weatherIcon)}</div>
@ -1372,8 +1373,8 @@ export function SpotMap({
const details = document.createElement('div');
details.className = 'space-y-1.5 mb-3 text-xs text-gray-300';
const startDate = new Date(evt.start * 1000);
const dateStr = startDate.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
const timeStr = startDate.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
const dateStr = startDate.toLocaleDateString(dateLocaleCode(), { weekday: 'short', month: 'short', day: 'numeric' });
const timeStr = startDate.toLocaleTimeString(dateLocaleCode(), { hour: 'numeric', minute: '2-digit' });
details.appendChild(popupRow('🗓️', `${dateStr} · ${timeStr}`));
if (evt.location) {
details.appendChild(popupRow('📍', evt.location));

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
@ -72,12 +73,12 @@ function formatDateRange(startDate?: number, endDate?: number, withTime = false)
});
}
const startStr = start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
const startStr = start.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric' });
if (!endDate) return startStr;
const end = new Date(endDate * 1000);
const endStr = end.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
const endStr = end.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' });
return `${startStr} - ${endStr}`;
}

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { Link } from 'react-router-dom';
import type { NostrEvent } from '@nostrify/nostrify';
import { Star, ThumbsUp, ArrowUpRight } from 'lucide-react';
@ -28,7 +29,7 @@ function ReviewRow({ ev, selfPubkey }: { ev: NostrEvent; selfPubkey: string }) {
const picture = author.data?.metadata?.picture;
const given = ev.pubkey === selfPubkey;
const date = new Date(ev.created_at * 1000).toLocaleDateString();
const date = new Date(ev.created_at * 1000).toLocaleDateString(dateLocaleCode());
return (
<div className="flex items-start gap-3 rounded-xl border border-white/8 bg-white/[0.03] p-3">

View file

@ -13,7 +13,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { MessageSquare, ChevronDown, ChevronRight, MoreHorizontal, Flag } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { genUserName } from '@/lib/genUserName';
interface CommentProps {

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState, useEffect, useCallback } from 'react';
import { useCashu } from '@/hooks/wallet/useCashu';
import { useMintManager } from '@/hooks/wallet/useMintManager';
@ -637,7 +638,7 @@ function CashuWalletCardNative() {
{entry.type === 'receive' && t('walletUi.cashu.receivedToken')}
</div>
<div className="text-xs text-muted-foreground">
{new Date(entry.timestamp).toLocaleDateString()}
{new Date(entry.timestamp).toLocaleDateString(dateLocaleCode())}
</div>
</div>
</div>

View file

@ -7,6 +7,7 @@ import {
translations,
type Language,
} from './language';
import { setDateLocale } from '@/lib/dateFnsI18n';
interface LanguageProviderProps {
children: ReactNode;
@ -73,6 +74,11 @@ export function LanguageProvider({ children }: LanguageProviderProps) {
// Currently no RTL languages supported, but ready for future
const isRTL = false;
// Keep date-fns + native toLocale* formatting in the app language. Called
// during render (before children) so the first paint is already in-language;
// it is an idempotent module assignment, not React state.
setDateLocale(language);
return (
<LanguageContext.Provider value={{ language, setLanguage, t, isRTL }}>
{children}

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState, useCallback } from 'react';
import { useCVMClient } from '@/hooks/useCVMClient';
import { callSasquatchAI, parseToolCall, type ChatTurn } from '@/lib/sasquatchBrain';
@ -64,7 +65,7 @@ export interface CrewContext {
function fmtDate(unix: number): string {
try {
return new Date(unix * 1000).toLocaleDateString(undefined, {
return new Date(unix * 1000).toLocaleDateString(dateLocaleCode(), {
weekday: 'short',
month: 'short',
day: 'numeric',

View file

@ -14,6 +14,7 @@
* Verification is handled separately by useSasquatchPro.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useCallback, useState } from 'react';
import { useNostr } from '@nostrify/react';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@ -59,7 +60,7 @@ export function usePurchasePro() {
const sendConfirmationDM = useCallback(
async (tier: ProTier, expiry: number) => {
if (!user?.pubkey || !user.signer?.nip44) return;
const until = new Date(expiry * 1000).toLocaleDateString(undefined, {
const until = new Date(expiry * 1000).toLocaleDateString(dateLocaleCode(), {
year: 'numeric', month: 'long', day: 'numeric',
});
const content = [

64
src/lib/dateFnsI18n.ts Normal file
View file

@ -0,0 +1,64 @@
/**
* Locale-aware date/number formatting.
*
* The app ships English + Spanish, but `date-fns` calls `formatDistanceToNow`
* with the default (English) locale and native `Date.toLocaleDateString()`
* falls back to the *runtime* locale, not the app language. This module keeps a
* single module-level locale in sync with `LanguageProvider` and re-exports all
* of `date-fns` with `formatDistanceToNow` / `formatDistance` / `format`
* overridden to apply it. Import from here instead of `date-fns` directly.
*
* `dateLocaleCode()` feeds native `toLocaleDateString/TimeString` calls so a
* Spanish user sees "5 ene" rather than "Jan 5".
*/
export * from 'date-fns';
import {
formatDistanceToNow as _formatDistanceToNow,
formatDistanceToNowStrict as _formatDistanceToNowStrict,
formatDistance as _formatDistance,
format as _format,
} from 'date-fns';
import { enUS, es as esLocale } from 'date-fns/locale';
import type { Locale } from 'date-fns';
import type { Language } from '@/contexts/language';
let activeLocale: Locale = enUS;
let activeCode = 'en-US';
/** Called by LanguageProvider (during render) so children format in-language. */
export function setDateLocale(language: Language): void {
activeLocale = language === 'es' ? esLocale : enUS;
activeCode = language === 'es' ? 'es-ES' : 'en-US';
}
/** The active date-fns locale. */
export function dateLocale(): Locale {
return activeLocale;
}
/** BCP-47 code for native `toLocale*` / `Intl` APIs. */
export function dateLocaleCode(): string {
return activeCode;
}
type DistOptions = Parameters<typeof _formatDistanceToNow>[1];
type DistStrictOptions = Parameters<typeof _formatDistanceToNowStrict>[1];
type FormatDistanceOptions = Parameters<typeof _formatDistance>[2];
type FormatOptions = Parameters<typeof _format>[2];
export function formatDistanceToNow(date: Date | number, options?: DistOptions) {
return _formatDistanceToNow(date, { ...options, locale: options?.locale ?? activeLocale });
}
export function formatDistanceToNowStrict(date: Date | number, options?: DistStrictOptions) {
return _formatDistanceToNowStrict(date, { ...options, locale: options?.locale ?? activeLocale });
}
export function formatDistance(date: Date | number, baseDate: Date | number, options?: FormatDistanceOptions) {
return _formatDistance(date, baseDate, { ...options, locale: options?.locale ?? activeLocale });
}
export function format(date: Date | number, formatStr: string, options?: FormatOptions) {
return _format(date, formatStr, { ...options, locale: options?.locale ?? activeLocale });
}

View file

@ -10,6 +10,7 @@
* 35679 — Park Access Card (addressable). A venue/owner signs it for a rider.
* 1293 — Skill badge (regular). A witness signs it for a rider.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '@nostrify/nostrify';
@ -193,11 +194,11 @@ export function roleMeta(role: string) {
/** Short, locale-aware date (+ time range) for the event pass. */
export function formatCardDate(start: number, end?: number): string {
const s = new Date(start * 1000);
const date = s.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
const time = s.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
const date = s.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' });
const time = s.toLocaleTimeString(dateLocaleCode(), { hour: '2-digit', minute: '2-digit' });
if (end && end > start) {
const e = new Date(end * 1000);
return `${date} · ${time}–${e.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`;
return `${date} · ${time}–${e.toLocaleTimeString(dateLocaleCode(), { hour: '2-digit', minute: '2-digit' })}`;
}
return `${date} · ${time}`;
}

View file

@ -22,6 +22,8 @@
* where none is published we report that honestly instead of faking a route.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
/** Travel modes offered in the UI. */
export type TravelMode = 'foot' | 'bike' | 'moto' | 'car' | 'transit';
@ -513,7 +515,7 @@ export async function fetchStopDepartures(
export function formatClock(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return d.toLocaleTimeString(dateLocaleCode(), { hour: '2-digit', minute: '2-digit' });
}
// ─── Formatting helpers ──────────────────────────────────────────────────────

View file

@ -8,6 +8,7 @@
* the real travel sites for the parts we can't (and shouldn't) do ourselves.
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { SKATE_SPOT_KIND } from '@/lib/types';
// ─── Shared result shapes (mirror useCVMClient return items) ─────────────────
@ -127,7 +128,7 @@ export function buildItinerary(opts: {
const eventStops: ItineraryStop[] = opts.events.map((e) => ({
kind: 'event' as const,
title: e.title,
subtitle: e.start ? new Date(e.start).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : e.location,
subtitle: e.start ? new Date(e.start).toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric' }) : e.location,
url: e.url,
}));

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useSeoMeta } from '@unhead/react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { PageError } from '@/components/ErrorState';
@ -288,7 +289,7 @@ export default function CoachDetailPage() {
</div>
<p className="text-gray-400 flex items-center gap-2">
<Calendar className="h-4 w-4" />
{t('labUi.detail.joined')} {new Date(profile.createdAt * 1000).toLocaleDateString()}
{t('labUi.detail.joined')} {new Date(profile.createdAt * 1000).toLocaleDateString(dateLocaleCode())}
</p>
</div>
</div>
@ -550,7 +551,7 @@ export default function CoachDetailPage() {
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold text-white">{gig.title}</p>
<p className="text-xs text-gray-400">
{new Date(gig.start * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
{new Date(gig.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' })}
{gig.location ? ` · ${gig.location}` : ''}
</p>
</div>

View file

@ -1,4 +1,5 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { useNostr } from '@nostrify/react';
@ -264,7 +265,7 @@ export default function CollectionDetailPage() {
</Badge>
<span className="flex items-center gap-1">
<Calendar className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
{t('walletUi.collection.created', { date: new Date(collection.createdAt * 1000).toLocaleDateString() })}
{t('walletUi.collection.created', { date: new Date(collection.createdAt * 1000).toLocaleDateString(dateLocaleCode()) })}
</span>
</div>
</div>

View file

@ -20,7 +20,7 @@ import { EscrowPanel } from '@/components/EscrowPanel';
import { useDMRead } from '@/hooks/useDMRead';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import {
ArrowLeft,
Send,

View file

@ -18,7 +18,7 @@ import { useDMRead } from '@/hooks/useDMRead';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import {
MessageCircle,
Plus,

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState, useEffect } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useNavigate, useParams } from 'react-router-dom';
@ -347,8 +348,8 @@ export default function EditEventPage() {
if (timeChanged) {
parts.push(
t('placesUi.editEvent.newTime', {
date: startDateTime.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }),
time: startDateTime.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }),
date: startDateTime.toLocaleDateString(dateLocaleCode(), { weekday: 'short', month: 'short', day: 'numeric' }),
time: startDateTime.toLocaleTimeString(dateLocaleCode(), { hour: 'numeric', minute: '2-digit' }),
}),
);
}

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useParams, Link } from 'react-router-dom';
@ -51,7 +52,7 @@ function eventTypeLabel(t: (key: string) => string, type: string): string {
function formatTime(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toLocaleTimeString('en-US', {
return date.toLocaleTimeString(dateLocaleCode(), {
hour: 'numeric',
minute: '2-digit',
});
@ -109,7 +110,7 @@ export default function EventDetailPage() {
const bits: string[] = [];
if (event.start) {
bits.push(
new Date(event.start * 1000).toLocaleDateString(undefined, {
new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), {
weekday: 'short',
month: 'short',
day: 'numeric',
@ -440,7 +441,7 @@ export default function EventDetailPage() {
<div className="flex items-center gap-3 p-4">
<div className="flex h-11 w-11 flex-shrink-0 flex-col items-center justify-center rounded-lg border-2 border-[#ccff00]/40 bg-[#ccff00]/10">
<span className="text-[9px] font-black uppercase leading-none text-[#ccff00]">
{new Date(event.start * 1000).toLocaleDateString(undefined, { month: 'short' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })}
</span>
<span className="text-lg font-black leading-none text-white">
{new Date(event.start * 1000).getDate()}
@ -449,7 +450,7 @@ export default function EventDetailPage() {
<div className="min-w-0">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-500">{t('placesUi.eventDetail.date')}</p>
<p className="truncate text-sm font-bold text-white">
{new Date(event.start * 1000).toLocaleDateString(undefined, { weekday: 'long' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { weekday: 'long' })}
</p>
</div>
</div>
@ -524,13 +525,13 @@ export default function EventDetailPage() {
<CardContent className="pt-6">
<div className="text-center">
<div className="text-sm font-medium text-primary uppercase mb-1">
{new Date(event.start * 1000).toLocaleDateString('en-US', { month: 'long' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'long' })}
</div>
<div className="text-5xl font-bold mb-2">
{new Date(event.start * 1000).getDate()}
</div>
<div className="text-muted-foreground">
{new Date(event.start * 1000).toLocaleDateString('en-US', { weekday: 'long' })}
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { weekday: 'long' })}
</div>
<div className="text-lg font-medium mt-2">
{formatTime(event.start)}

View file

@ -14,7 +14,7 @@ import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
import { useGroupConversation, useSendDirectMessage } from '@/hooks/useDirectMessages';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
import { ArrowLeft, Lock, Loader2, RefreshCw, Send, Users } from 'lucide-react';
import { cn } from '@/lib/utils';

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
@ -243,7 +244,7 @@ export default function ListingDetailPage() {
)}
<div className="flex items-center gap-2 text-gray-500">
<Calendar className="h-4 w-4" />{' '}
{t('marketUi.listingDetail.listed', { date: new Date(item.createdAt * 1000).toLocaleDateString() })}
{t('marketUi.listingDetail.listed', { date: new Date(item.createdAt * 1000).toLocaleDateString(dateLocaleCode()) })}
</div>
</CardContent>
</Card>

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { nip19 } from 'nostr-tools';
import { useParams, Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
@ -41,7 +42,7 @@ import { useTranslation } from '@/contexts/language';
/** Format a "member since" year from a kind-0 event timestamp (seconds). */
function profileEventCreatedAt(createdAt: number | undefined): string | undefined {
if (!createdAt) return undefined;
return new Date(createdAt * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short' });
return new Date(createdAt * 1000).toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'short' });
}
/** A stat cell for the at-a-glance grid. Clickable (jumps to its section) when

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useEffect, useRef, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
@ -65,7 +66,7 @@ function NostrPreviewModal({
const displayName = metadata?.name ?? genUserName(user?.pubkey ?? '');
const avatar = metadata?.picture;
const now = new Date();
const timeStr = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
const timeStr = now.toLocaleTimeString(dateLocaleCode(), { hour: 'numeric', minute: '2-digit' });
const [editing, setEditing] = useState(false);
const [editedText, setEditedText] = useState(issue.nostrText);
@ -397,7 +398,7 @@ function SpotOfTheWeekBlock({ spot }: { spot: SpotOfTheWeek }) {
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatIssueDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', {
return new Date(iso).toLocaleDateString(dateLocaleCode(), {
year: 'numeric', month: 'long', day: 'numeric',
});
}
@ -547,7 +548,7 @@ function LiveIssueCard({ issue }: { issue: SkateNewsletterIssue }) {
pubkey: issue.pubkey,
identifier: issue.identifier,
});
const date = new Date(issue.publishedAt * 1000).toLocaleDateString('en-US', {
const date = new Date(issue.publishedAt * 1000).toLocaleDateString(dateLocaleCode(), {
year: 'numeric',
month: 'long',
day: 'numeric',

View file

@ -13,6 +13,7 @@
* Print-ready: credit-card size (85.6×54mm), @media print isolates card
*/
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
@ -150,7 +151,7 @@ function BadgeEventRow({ event }: { event: NostrEvent }) {
</p>
</div>
<div className="text-[9px] text-gray-600 flex-shrink-0">
{new Date(event.created_at * 1000).toLocaleDateString()}
{new Date(event.created_at * 1000).toLocaleDateString(dateLocaleCode())}
</div>
</div>
);

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useParams, Link, useNavigate } from 'react-router-dom';
@ -480,7 +481,7 @@ export default function ShopDetailPage() {
<p className="font-medium truncate">{displayName}</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Calendar className="h-3 w-3" />
{new Date(shop.createdAt * 1000).toLocaleDateString()}
{new Date(shop.createdAt * 1000).toLocaleDateString(dateLocaleCode())}
</p>
</div>
</div>

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
@ -100,7 +101,7 @@ function CollectionCard({
<div className="flex items-center justify-between text-[10px] sm:text-xs text-gray-500">
<span className="flex items-center gap-1">
<Calendar className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
{new Date(collection.createdAt * 1000).toLocaleDateString()}
{new Date(collection.createdAt * 1000).toLocaleDateString(dateLocaleCode())}
</span>
<span className="flex items-center gap-1 text-[#ccff00] sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
{t('mapUi.view')}

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useParams, Link, useNavigate } from 'react-router-dom';
@ -1047,7 +1048,7 @@ export default function SpotDetailPage() {
<div className="grid grid-cols-3 gap-2">
{weatherData.forecast.daily.map((day, idx) => {
const date = new Date(day.date);
const dayName = idx === 0 ? t('spotUi.today') : idx === 1 ? t('spotUi.tomorrow') : date.toLocaleDateString('en-US', { weekday: 'short' });
const dayName = idx === 0 ? t('spotUi.today') : idx === 1 ? t('spotUi.tomorrow') : date.toLocaleDateString(dateLocaleCode(), { weekday: 'short' });
return (
<div key={day.date} className="text-center p-2 rounded-lg bg-background/50">
<div className="text-xs font-semibold text-muted-foreground mb-1">{dayName}</div>
@ -1149,7 +1150,7 @@ export default function SpotDetailPage() {
</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Calendar className="h-3 w-3" />
{new Date(spot.createdAt * 1000).toLocaleDateString()}
{new Date(spot.createdAt * 1000).toLocaleDateString(dateLocaleCode())}
</p>
</div>
</Link>

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useSeoMeta } from '@unhead/react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
@ -149,10 +150,10 @@ export default function TourDetailPage() {
minute: '2-digit',
});
}
const startStr = start.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
const startStr = start.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' });
if (tour.endDate) {
const end = new Date(tour.endDate * 1000);
const endStr = end.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
const endStr = end.toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' });
return `${startStr} → ${endStr}`;
}
return startStr;

View file

@ -1,3 +1,4 @@
import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
@ -281,7 +282,7 @@ export default function TradesPage() {
{counterparty ? <>{t('marketUi.trades.with')} <PeerName pubkey={counterparty} /></> : t('marketUi.trades.roleTrade').toLowerCase()}
</p>
<p className="text-[11px] text-gray-500">
{new Date(ev.created_at * 1000).toLocaleDateString()}
{new Date(ev.created_at * 1000).toLocaleDateString(dateLocaleCode())}
{state !== 'valid' && ` · ${state}`}
</p>
</div>