From ebd21c9df313a135a4b93c6945b63eb3b7cde668 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Mon, 20 Oct 2025 03:27:19 +0100 Subject: [PATCH] feat(ui): use locale-aware compact number formatting --- .../layout/main/card/BlueskyPostCard.svelte | 33 +++++---------- .../layout/main/card/ProfileCard.svelte | 29 ++++++-------- src/lib/utils/formatNumber.ts | 40 ++++++++++++++----- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/lib/components/layout/main/card/BlueskyPostCard.svelte b/src/lib/components/layout/main/card/BlueskyPostCard.svelte index 13d8f06..eee26b9 100644 --- a/src/lib/components/layout/main/card/BlueskyPostCard.svelte +++ b/src/lib/components/layout/main/card/BlueskyPostCard.svelte @@ -3,6 +3,7 @@ import { Card } from '$lib/components/ui'; import { fetchLatestBlueskyPost, type BlueskyPost } from '$lib/services/atproto'; import { formatRelativeTime } from '$lib/utils/formatDate'; + import { formatCompactNumber } from '$lib/utils/formatNumber'; import { Heart, Repeat2, MessageCircle, ExternalLink, X } from '@lucide/svelte'; let post: BlueskyPost | null = null; @@ -10,6 +11,9 @@ let error: string | null = null; let lightboxImage: { url: string; alt: string } | null = null; + // Detect system locale, fallback to en-GB + const locale = typeof navigator !== 'undefined' ? navigator.language || 'en-GB' : 'en-GB'; + onMount(async () => { try { post = await fetchLatestBlueskyPost(); @@ -20,16 +24,7 @@ } }); - function formatNumber(num?: number): string { - if (!num) return '0'; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`; - if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`; - return num.toString(); - } - function getPostUrl(uri: string): string { - // Convert AT URI to bsky.app URL - // Format: at://did:plc:xxx/app.bsky.feed.post/rkey const parts = uri.split('/'); const did = parts[2]; const rkey = parts[4]; @@ -50,13 +45,8 @@ document.body.style.overflow = ''; } - // Render rich text with facets (links, mentions, hashtags) function renderRichText(text: string, facets?: any[]): string { - if (!facets || facets.length === 0) { - return escapeHtml(text); - } - - // Sort facets by byteStart to process them in order + if (!facets || facets.length === 0) return escapeHtml(text); const sortedFacets = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart); let result = ''; @@ -64,10 +54,7 @@ for (const facet of sortedFacets) { const { byteStart, byteEnd } = facet.index; - - // Add text before this facet result += escapeHtml(text.slice(lastIndex, byteStart)); - const facetText = text.slice(byteStart, byteEnd); const feature = facet.features?.[0]; @@ -88,9 +75,7 @@ lastIndex = byteEnd; } - // Add remaining text after last facet result += escapeHtml(text.slice(lastIndex)); - return result; } @@ -284,7 +269,7 @@ {/if} - + {#if depth === 0 || (depth === 1 && !postData.quotedPost)}
{#if postData.replyCount !== undefined} @@ -293,21 +278,21 @@ class="h-{isQuoted ? '3' : '4'} w-{isQuoted ? '3' : '4'}" aria-hidden="true" /> - {formatNumber(postData.replyCount)} + {formatCompactNumber(postData.replyCount, locale)}
{/if} {#if postData.repostCount !== undefined}
{/if} {#if postData.likeCount !== undefined}
{/if} diff --git a/src/lib/components/layout/main/card/ProfileCard.svelte b/src/lib/components/layout/main/card/ProfileCard.svelte index e0d22de..5d8bae6 100644 --- a/src/lib/components/layout/main/card/ProfileCard.svelte +++ b/src/lib/components/layout/main/card/ProfileCard.svelte @@ -3,6 +3,7 @@ import { Card } from '$lib/components/ui'; import { fetchProfile, type ProfileData } from '$lib/services/atproto'; import LinkCard from './LinkCard.svelte'; + import { formatCompactNumber } from '$lib/utils/formatNumber'; let profile: ProfileData | null = null; let loading = true; @@ -10,6 +11,9 @@ let imageLoaded = false; let bannerLoaded = false; + // Detect system locale, fallback to en-GB + const locale = typeof navigator !== 'undefined' ? navigator.language || 'en-GB' : 'en-GB'; + onMount(async () => { try { profile = await fetchProfile(); @@ -19,13 +23,6 @@ loading = false; } }); - - function formatNumber(num?: number): string { - if (!num) return '0'; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`; - if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`; - return num.toString(); - }
@@ -118,21 +115,21 @@
- {formatNumber(safeProfile.postsCount)} + + {formatCompactNumber(safeProfile.postsCount, locale)} + Posts
- {formatNumber(safeProfile.followersCount)} + + {formatCompactNumber(safeProfile.followersCount, locale)} + Followers
- {formatNumber(safeProfile.followsCount)} + + {formatCompactNumber(safeProfile.followsCount, locale)} + Following
diff --git a/src/lib/utils/formatNumber.ts b/src/lib/utils/formatNumber.ts index ce64a69..4383463 100644 --- a/src/lib/utils/formatNumber.ts +++ b/src/lib/utils/formatNumber.ts @@ -3,24 +3,42 @@ */ /** - * Formats large numbers into compact human-readable format + * Determines the effective locale, preferring system locale with fallback to 'en-GB'. + */ +function getLocale(locale?: string): string { + return ( + locale || + (typeof navigator !== 'undefined' && navigator.language) || + 'en-GB' + ); +} + +/** + * Formats large numbers into a compact, human-readable format. + * Automatically adapts to the given or system locale. * @param num - The number to format + * @param locale - Optional locale string (defaults to system or 'en-GB') * @returns Formatted string (e.g., "1.2K", "3.4M") */ -export function formatCompactNumber(num?: number): string { - if (!num) return '0'; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`; - if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`; - return num.toString(); +export function formatCompactNumber(num?: number, locale?: string): string { + if (num === undefined || num === null) return '0'; + const effectiveLocale = getLocale(locale); + + return new Intl.NumberFormat(effectiveLocale, { + notation: 'compact', + compactDisplay: 'short', + maximumFractionDigits: 1 + }).format(num); } /** - * Formats a number with thousand separators + * Formats a number with thousand separators. + * Automatically adapts to the given or system locale. * @param num - The number to format - * @param locale - The locale to use (default: system locale) + * @param locale - Optional locale string (defaults to system or 'en-GB') * @returns Formatted string (e.g., "1,234,567") */ export function formatNumber(num: number, locale?: string): string { - const userLocale = locale || (typeof navigator !== 'undefined' ? navigator.language : 'en-GB'); - return new Intl.NumberFormat(userLocale).format(num); -} + const effectiveLocale = getLocale(locale); + return new Intl.NumberFormat(effectiveLocale).format(num); +} \ No newline at end of file -- 2.51.2