diff --git a/AGENTS.md b/AGENTS.md index 9f8bc8478b8..fede2fcfc3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,6 +154,7 @@ pnpm events-import # Import community events 6. **Consider i18n** - All user-facing text should be translatable (use `getTranslations` and `getLocale`) 7. **Mobile-first** - Design for mobile, enhance for desktop 8. **Accessibility** - Use Radix primitives, semantic HTML +9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales. ### Component Development diff --git a/app/[locale]/10years/_components/torchHoldersData.ts b/app/[locale]/10years/_components/torchHoldersData.ts index 33202a057e1..57beb3a075a 100644 --- a/app/[locale]/10years/_components/torchHoldersData.ts +++ b/app/[locale]/10years/_components/torchHoldersData.ts @@ -1,3 +1,5 @@ +import { dateTimeFormat } from "@/lib/utils/date" + /** * Pre-computed static torch holders data * This data is final and will not be updated (the torch has been burned) @@ -229,13 +231,13 @@ export const extractTwitterHandle = (twitterUrl: string): string | null => { export const formatTorchDate = (timestamp: number): string => { const date = new Date(timestamp * 1000) - const month = date.toLocaleDateString("en-US", { month: "long" }) + const month = dateTimeFormat("en-US", { month: "long" }).format(date) const day = date.getDate().toString().padStart(2, "0") - const time = date.toLocaleTimeString("en-US", { + const time = dateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", hour12: true, - }) + }).format(date) return `${month} ${day}, ${time}` } diff --git a/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx b/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx index 5b46b0dff26..9ae2fcd1416 100644 --- a/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx +++ b/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx @@ -5,6 +5,8 @@ import { useLocale } from "next-intl" import { Progress } from "@/components/ui/progress" +import { dateTimeFormat } from "@/lib/utils/date" + import { type BadgeWithOwned } from "../CollectiblesContent" import useTranslation from "@/hooks/useTranslation" @@ -38,7 +40,7 @@ const CollectiblesProgress = ({ badges }: CollectiblesProgressProps) => { {contributorSinceYear < Infinity && (

{t("page-collectibles-contributing-since")}:{" "} - {new Intl.DateTimeFormat(locale, { + {dateTimeFormat(locale, { year: "numeric", }).format(new Date().setFullYear(contributorSinceYear))}

diff --git a/app/[locale]/collectibles/page.tsx b/app/[locale]/collectibles/page.tsx index ba582b15753..626cf8aa87d 100644 --- a/app/[locale]/collectibles/page.tsx +++ b/app/[locale]/collectibles/page.tsx @@ -15,6 +15,7 @@ import { Section } from "@/components/ui/section" import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getMetadata } from "@/lib/utils/metadata" +import { numberFormat } from "@/lib/utils/numbers" import { getRequiredNamespacesForPage } from "@/lib/utils/translations" import CollectiblesPage from "./_components/Collectibles/lazy" @@ -111,7 +112,9 @@ export default async function Page(props: { params: Promise }) { {/* Minted */}
- {stats.collectorsCount?.toLocaleString(locale) ?? "-"} + {stats.collectorsCount + ? numberFormat(locale).format(stats.collectorsCount) + : "-"}
{t("page-collectibles-stats-minted")} @@ -120,7 +123,9 @@ export default async function Page(props: { params: Promise }) { {/* Collectors */}
- {stats.uniqueAddressesCount?.toLocaleString(locale) ?? "-"} + {stats.uniqueAddressesCount + ? numberFormat(locale).format(stats.uniqueAddressesCount) + : "-"}
{t("page-collectibles-stats-collectors")} @@ -129,7 +134,9 @@ export default async function Page(props: { params: Promise }) { {/* Unique Badges */}
- {stats.collectiblesCount?.toLocaleString(locale) ?? "-"} + {stats.collectiblesCount + ? numberFormat(locale).format(stats.collectiblesCount) + : "-"}
{t("page-collectibles-stats-unique-badges")} diff --git a/app/[locale]/developers/tools/_components/ToolModalContents.tsx b/app/[locale]/developers/tools/_components/ToolModalContents.tsx index 4d0ba038dfe..46d6ae60a4d 100644 --- a/app/[locale]/developers/tools/_components/ToolModalContents.tsx +++ b/app/[locale]/developers/tools/_components/ToolModalContents.tsx @@ -10,6 +10,7 @@ import { ButtonLink } from "@/components/ui/buttons/Button" import { Tag, TagsInlineText } from "@/components/ui/tag" import { formatDate, getValidDate } from "@/lib/utils/date" +import { numberFormat } from "@/lib/utils/numbers" import { isExternal } from "@/lib/utils/url" import { DEV_TOOL_CATEGORY_SLUGS } from "../constants" @@ -125,7 +126,7 @@ const ToolModalContents = async ({ tool }: { tool: DeveloperTool }) => { className="text-sm" title={t("page-developers-tools-stats-stargazers")} > - ({new Intl.NumberFormat(locale).format(stargazers)} + ({numberFormat(locale).format(stargazers)} ) @@ -139,7 +140,7 @@ const ToolModalContents = async ({ tool }: { tool: DeveloperTool }) => { title={t("page-developers-tools-stats-downloads")} > ( - {new Intl.NumberFormat(locale, { + {numberFormat(locale, { notation: "compact", }).format(downloads)} diff --git a/app/[locale]/layer-2/_components/layer-2.tsx b/app/[locale]/layer-2/_components/layer-2.tsx index 73656d28d64..52587996cd9 100644 --- a/app/[locale]/layer-2/_components/layer-2.tsx +++ b/app/[locale]/layer-2/_components/layer-2.tsx @@ -12,6 +12,8 @@ import Translation from "@/components/Translation" import { ButtonLink } from "@/components/ui/buttons/Button" import InlineLink from "@/components/ui/Link" +import { numberFormat } from "@/lib/utils/numbers" + import { Rollups } from "@/data/networks/networks" import useTranslation from "@/hooks/useTranslation" @@ -126,12 +128,10 @@ const Layer2Hub = ({

$ - {( - growThePieData.dailyTxCosts["ethereum"] || 0 - ).toLocaleString(locale as Lang, { + {numberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2, - })} + }).format(growThePieData.dailyTxCosts["ethereum"] || 0)}

{t("page-layer-2-blockchain-transaction-cost")} @@ -143,10 +143,12 @@ const Layer2Hub = ({

$ - {medianTxCost.toLocaleString(locale as Lang, { + {numberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 3, - })} + }).format( + typeof medianTxCost === "number" ? medianTxCost : 0 + )}

{t("page-layer-2-networks-transaction-cost")} diff --git a/app/[locale]/resources/_components/SlotCountdown.tsx b/app/[locale]/resources/_components/SlotCountdown.tsx index 2a5bf5fa062..3522964762f 100644 --- a/app/[locale]/resources/_components/SlotCountdown.tsx +++ b/app/[locale]/resources/_components/SlotCountdown.tsx @@ -5,6 +5,8 @@ import { useLocale } from "next-intl" import RadialChart from "@/components/RadialChart" +import { numberFormat } from "@/lib/utils/numbers" + const SlotCountdownChart = ({ children }: { children: string }) => { const [timeToNextBlock, setTimeToNextBlock] = useState(12) const locale = useLocale() @@ -28,7 +30,7 @@ const SlotCountdownChart = ({ children }: { children: string }) => { { @@ -106,7 +108,7 @@ const UpgradeCountdown = () => { ) : (

Live Since{" "} - {new Intl.DateTimeFormat(locale, { timeZone: "UTC" }).format( + {dateTimeFormat(locale, { timeZone: "UTC" }).format( new Date(upgradeDate) )}
diff --git a/app/[locale]/resources/page.tsx b/app/[locale]/resources/page.tsx index 6c5238beaf9..10db367e5a1 100644 --- a/app/[locale]/resources/page.tsx +++ b/app/[locale]/resources/page.tsx @@ -17,6 +17,7 @@ import TabNav, { StickyContainer } from "@/components/ui/TabNav" import { cn } from "@/lib/utils/cn" import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getMetadata } from "@/lib/utils/metadata" +import { numberFormat } from "@/lib/utils/numbers" import { GITHUB_REPO_URL } from "@/lib/constants" @@ -56,7 +57,7 @@ const Page = async (props: { params: Promise }) => { // Extract blob stats directly (getBlobscanStats returns BlobscanStats, not wrapped in MetricReturnData) const blobStats = { avgBlobFee: blobscanOverallStats.avgBlobFee, - totalBlobs: new Intl.NumberFormat(undefined, { + totalBlobs: numberFormat(locale, { notation: "compact", maximumFractionDigits: 1, }).format(blobscanOverallStats.totalBlobs), diff --git a/app/[locale]/resources/utils.tsx b/app/[locale]/resources/utils.tsx index dad5d19be0e..20a1260152b 100644 --- a/app/[locale]/resources/utils.tsx +++ b/app/[locale]/resources/utils.tsx @@ -1,7 +1,5 @@ import { getLocale, getTranslations } from "next-intl/server" -import { Lang } from "@/lib/types" - import BigNumber from "@/components/BigNumber" import SectionIconArrowsFullscreen from "@/components/icons/arrows-fullscreen.svg" import SectionIconEthGlyph from "@/components/icons/eth-glyph.svg" @@ -10,7 +8,6 @@ import SectionIconHeartPulse from "@/components/icons/heart-pulse.svg" import SectionIconPrivacy from "@/components/icons/privacy.svg" import { formatSmallUSD } from "@/lib/utils/numbers" -import { getLocaleForNumberFormat } from "@/lib/utils/translations" import { SlotCountdownChart, @@ -64,7 +61,6 @@ export const getResources = async ({ }): Promise => { const locale = await getLocale() const t = await getTranslations({ locale, namespace: "page-resources" }) - const localeForNumberFormat = getLocaleForNumberFormat(locale as Lang) // Fetch ETH price using the new data-layer function (already cached) const ethPrice = await getEthPrice() @@ -82,7 +78,7 @@ export const getResources = async ({ value: formatSmallUSD( // Converting value from wei to USD avgBlobFee * 1e-18 * ethPrice.value, - localeForNumberFormat + locale ), } @@ -91,7 +87,7 @@ export const getResources = async ({ ? { error: txCostsMedianUsd.error } : { ...txCostsMedianUsd, - value: formatSmallUSD(txCostsMedianUsd.value, localeForNumberFormat), + value: formatSmallUSD(txCostsMedianUsd.value, locale), } const networkBoxes: DashboardBox[] = [ diff --git a/app/[locale]/roadmap/_components/ReleaseCarousel.tsx b/app/[locale]/roadmap/_components/ReleaseCarousel.tsx index 7dded3f6fae..3aee5f05a28 100644 --- a/app/[locale]/roadmap/_components/ReleaseCarousel.tsx +++ b/app/[locale]/roadmap/_components/ReleaseCarousel.tsx @@ -16,7 +16,7 @@ import { } from "@/components/ui/carousel" import { cn } from "@/lib/utils/cn" -import { formatDate } from "@/lib/utils/date" +import { dateTimeFormat, formatDate } from "@/lib/utils/date" import { getReleasesData, Release } from "@/data/roadmap/releases" @@ -100,7 +100,7 @@ const ReleaseCarousel = () => { return "" if ("plannedReleaseYear" in release && release.plannedReleaseYear) - return new Intl.DateTimeFormat(locale, { + return dateTimeFormat(locale, { year: "numeric", }).format(new Date(Number(release.plannedReleaseYear), 0, 1)) diff --git a/app/[locale]/stablecoins/page.tsx b/app/[locale]/stablecoins/page.tsx index c0f7fbcc016..0c95a782367 100644 --- a/app/[locale]/stablecoins/page.tsx +++ b/app/[locale]/stablecoins/page.tsx @@ -33,6 +33,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { cn } from "@/lib/utils/cn" import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getMetadata } from "@/lib/utils/metadata" +import { numberFormat } from "@/lib/utils/numbers" import { getRequiredNamespacesForPage } from "@/lib/utils/translations" import { stablecoins } from "./data" @@ -118,7 +119,7 @@ async function Page(props: { params: Promise }) { .sort((a, b) => b.market_cap - a.market_cap) .map(({ market_cap, ...rest }) => ({ ...rest, - marketCap: new Intl.NumberFormat("en-US", { + marketCap: numberFormat(locale, { style: "currency", currency: "USD", minimumFractionDigits: 0, diff --git a/app/[locale]/utils.ts b/app/[locale]/utils.ts index cc4484c3aa8..baf20b1b1f2 100644 --- a/app/[locale]/utils.ts +++ b/app/[locale]/utils.ts @@ -7,10 +7,10 @@ import { getTranslations } from "next-intl/server" import type { AllHomepageActivityData, Lang, StatsBoxMetric } from "@/lib/types" -import { getLocaleForNumberFormat } from "@/lib/utils/translations" +import { numberFormat } from "@/lib/utils/numbers" const formatLargeUSD = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { style: "currency", currency: "USD", notation: "compact", @@ -20,7 +20,7 @@ const formatLargeUSD = (value: number, locale: string): string => { } const formatSmallUSD = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { style: "currency", currency: "USD", notation: "compact", @@ -30,7 +30,7 @@ const formatSmallUSD = (value: number, locale: string): string => { } const formatLargeNumber = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { notation: "compact", minimumSignificantDigits: 3, maximumSignificantDigits: 4, @@ -49,8 +49,6 @@ export const getActivity = async ( ): Promise => { const t = await getTranslations("page-index") - const localeForNumberFormat = getLocaleForNumberFormat(locale) - const hasEthStakerAndPriceData = "value" in totalEthStaked && "value" in ethPrice const totalStakedInUsd = hasEthStakerAndPriceData @@ -68,7 +66,7 @@ export const getActivity = async ( } : { ...totalEthStaked, - value: formatLargeUSD(totalStakedInUsd, localeForNumberFormat), + value: formatLargeUSD(totalStakedInUsd, locale), } const valueLocked = @@ -76,7 +74,7 @@ export const getActivity = async ( ? { error: totalValueLocked.error } : { ...totalValueLocked, - value: formatLargeUSD(totalValueLocked.value, localeForNumberFormat), + value: formatLargeUSD(totalValueLocked.value, locale), } const txs = @@ -84,7 +82,7 @@ export const getActivity = async ( ? { error: txCount.error } : { ...txCount, - value: formatLargeNumber(txCount.value, localeForNumberFormat), + value: formatLargeNumber(txCount.value, locale), } const medianTxCost = @@ -92,7 +90,7 @@ export const getActivity = async ( ? { error: txCostsMedianUsd.error } : { ...txCostsMedianUsd, - value: formatSmallUSD(txCostsMedianUsd.value, localeForNumberFormat), + value: formatSmallUSD(txCostsMedianUsd.value, locale), } const metrics: StatsBoxMetric[] = [ diff --git a/i18n.config.json b/i18n.config.json index 7c5b7605fd9..bc5e72fa8b1 100644 --- a/i18n.config.json +++ b/i18n.config.json @@ -4,224 +4,174 @@ "crowdinCode": "en", "name": "English", "localName": "English", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "ar", "crowdinCode": "ar", "name": "Arabic", "localName": "العربية", - "langDir": "rtl", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "rtl" }, { "code": "bn", "crowdinCode": "bn", "name": "Bengali", "localName": "বাংলা", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "cs", "crowdinCode": "cs", "name": "Czech", "localName": "Čeština", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "de", "crowdinCode": "de", "name": "German", "localName": "Deutsch", - "langDir": "ltr", - "dateFormat": "DD/MM/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "es", "crowdinCode": "es-EM", "name": "Spanish", "localName": "Español", - "langDir": "ltr", - "dateFormat": "DD/MM/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "fr", "crowdinCode": "fr", "name": "French", "localName": "Français", - "langDir": "ltr", - "dateFormat": "DD/MM/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "hi", "crowdinCode": "hi", "name": "Hindi", "localName": "हिन्दी", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "id", "crowdinCode": "id", "name": "Indonesian", "localName": "Bahasa Indonesia", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "it", "crowdinCode": "it", "name": "Italian", "localName": "Italiano", - "langDir": "ltr", - "dateFormat": "DD/MM/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "ja", "crowdinCode": "ja", "name": "Japanese", "localName": "日本語", - "langDir": "ltr", - "dateFormat": "YYYY/MM/DD", - "validISO639_1": true + "langDir": "ltr" }, { "code": "ko", "crowdinCode": "ko", "name": "Korean", "localName": "한국어", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "mr", "crowdinCode": "mr", "name": "Marathi", "localName": "मराठी", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "pl", "crowdinCode": "pl", "name": "Polish", "localName": "Polski", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "pt-br", "crowdinCode": "pt-BR", "name": "Portuguese (Brazilian)", "localName": "Português", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "ru", "crowdinCode": "ru", "name": "Russian", "localName": "Pусский", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "sw", "crowdinCode": "sw", "name": "Swahili", "localName": "Kiswahili", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "ta", "crowdinCode": "ta", "name": "Tamil", "localName": "தமிழ்", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "te", "crowdinCode": "te", "name": "Telugu", "localName": "తెలుగు", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "tr", "crowdinCode": "tr", "name": "Turkish", "localName": "Türkçe", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "uk", "crowdinCode": "uk", "name": "Ukrainian", "localName": "Українська", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "ur", "crowdinCode": "ur-IN", "name": "Urdu", "localName": "اردو", - "langDir": "rtl", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "rtl" }, { "code": "vi", "crowdinCode": "vi", "name": "Vietnamese", "localName": "Tiếng Việt", - "langDir": "ltr", - "dateFormat": "MM/DD/YYYY", - "validISO639_1": true + "langDir": "ltr" }, { "code": "zh-tw", "crowdinCode": "zh-TW", "name": "Chinese Traditional", "localName": "繁體中文", - "langDir": "ltr", - "dateFormat": "YYYY-MM-DD", - "validISO639_1": true + "langDir": "ltr" }, { "code": "zh", "crowdinCode": "zh-CN", "name": "Chinese Simplified", "localName": "简体中文", - "langDir": "ltr", - "dateFormat": "YYYY-MM-DD", - "validISO639_1": true + "langDir": "ltr" } ] diff --git a/src/components/BigNumber/index.tsx b/src/components/BigNumber/index.tsx index 24d03bcd183..3c72b9d7c58 100644 --- a/src/components/BigNumber/index.tsx +++ b/src/components/BigNumber/index.tsx @@ -4,7 +4,7 @@ import { Info } from "lucide-react" import { getLocale, getTranslations } from "next-intl/server" import { cn } from "@/lib/utils/cn" -import { isValidDate } from "@/lib/utils/date" +import { dateTimeFormat, isValidDate } from "@/lib/utils/date" import Tooltip from "../Tooltip" import Link from "../ui/Link" @@ -68,7 +68,7 @@ const BigNumber = async ({ const lastUpdatedDisplay = lastUpdated && isValidDate(lastUpdated) - ? new Intl.DateTimeFormat(locale, { + ? dateTimeFormat(locale, { dateStyle: "medium", }).format(new Date(lastUpdated)) : "" diff --git a/src/components/Content/what-are-apps/WhatAreAppsStories.tsx b/src/components/Content/what-are-apps/WhatAreAppsStories.tsx index 76d50d9f224..94c8719c619 100644 --- a/src/components/Content/what-are-apps/WhatAreAppsStories.tsx +++ b/src/components/Content/what-are-apps/WhatAreAppsStories.tsx @@ -9,6 +9,7 @@ import { Image } from "@/components/Image" import { Button, ButtonLink } from "@/components/ui/buttons/Button" import { cn } from "@/lib/utils/cn" +import { dateTimeFormat } from "@/lib/utils/date" const stories: Story[] = [ { @@ -119,11 +120,11 @@ const WhatAreAppsStories = () => {

- {new Date(story.date).toLocaleDateString("en-US", { + {dateTimeFormat("en-US", { year: "numeric", month: "long", day: "numeric", - })} + }).format(new Date(story.date))}

))} diff --git a/src/components/EthPriceCard.tsx b/src/components/EthPriceCard.tsx index 1b715c6b218..eeffcbee742 100644 --- a/src/components/EthPriceCard.tsx +++ b/src/components/EthPriceCard.tsx @@ -10,6 +10,7 @@ import Tooltip from "@/components/Tooltip" import InlineLink from "@/components/ui/Link" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import { Flex } from "./ui/flex" @@ -70,7 +71,7 @@ const EthPriceCard = ({ const hasData = "data" in state const formatPrice = (price: string) => - new Intl.NumberFormat(locale, { + numberFormat(locale, { style: "currency", currency: "USD", minimumFractionDigits: 2, @@ -78,7 +79,7 @@ const EthPriceCard = ({ }).format(+price) const formatPercentage = (amount: number): string => - new Intl.NumberFormat(locale, { + numberFormat(locale, { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2, diff --git a/src/components/FindWalletProductTable/WalletSubComponent.tsx b/src/components/FindWalletProductTable/WalletSubComponent.tsx index ceb5dfb1c1c..1606ddd1319 100644 --- a/src/components/FindWalletProductTable/WalletSubComponent.tsx +++ b/src/components/FindWalletProductTable/WalletSubComponent.tsx @@ -14,8 +14,8 @@ import Tooltip from "@/components/Tooltip" import InlineLink from "@/components/ui/Link" import { cn } from "@/lib/utils/cn" +import { getLocaleFormattedDate } from "@/lib/utils/date" import { trackCustomEvent } from "@/lib/utils/matomo" -import { getLocaleFormattedDate } from "@/lib/utils/time" import { useTranslation } from "@/hooks/useTranslation" diff --git a/src/components/GitStars.tsx b/src/components/GitStars.tsx index 4eb5d4e8727..bb0cd3a93cc 100644 --- a/src/components/GitStars.tsx +++ b/src/components/GitStars.tsx @@ -4,6 +4,8 @@ import Github from "@/components/icons/github.svg" import { Center, Flex } from "@/components/ui/flex" import { BaseLink, LinkProps } from "@/components/ui/Link" +import { numberFormat } from "@/lib/utils/numbers" + import Emoji from "./Emoji" type GitHubRepo = { @@ -18,8 +20,8 @@ type GitStarsProps = Omit & { const GitStars = ({ gitHubRepo, hideStars, ...props }: GitStarsProps) => { const locale = useLocale() - // Use Intl.NumberFormat to format the number for locale - const starsString = Intl.NumberFormat(locale, { + // Use numberFormat to format the number for locale + const starsString = numberFormat(locale, { compactDisplay: "short", }).format(gitHubRepo.stargazerCount) diff --git a/src/components/History/NetworkUpgradeSummary.tsx b/src/components/History/NetworkUpgradeSummary.tsx index 230219f1e85..0d94363f377 100644 --- a/src/components/History/NetworkUpgradeSummary.tsx +++ b/src/components/History/NetworkUpgradeSummary.tsx @@ -3,11 +3,10 @@ import { useEffect, useState } from "react" import { useLocale } from "next-intl" -import type { Lang } from "@/lib/types" - import { Flex, Stack } from "@/components/ui/flex" -import { getLocaleForNumberFormat } from "@/lib/utils/translations" +import { dateTimeFormat } from "@/lib/utils/date" +import { numberFormat } from "@/lib/utils/numbers" import networkUpgradeSummaryData from "@/data/networkUpgradeSummaryData" @@ -23,7 +22,6 @@ type NetworkUpgradeSummaryProps = { const NetworkUpgradeSummary = ({ name }: NetworkUpgradeSummaryProps) => { const [formattedUTC, setFormattedUTC] = useState("") const locale = useLocale() - const localeForStatsBoxNumbers = getLocaleForNumberFormat(locale as Lang) const { t } = useTranslation("page-history") const { @@ -39,7 +37,7 @@ const NetworkUpgradeSummary = ({ name }: NetworkUpgradeSummaryProps) => { // calculate date format only on the client side to avoid hydration issues useEffect(() => { const date = new Date(dateTimeAsString as string) - const formattedDate = date.toLocaleString(locale, { + const formattedDate = dateTimeFormat(locale, { timeZone: "UTC", month: "short", day: "numeric", @@ -47,7 +45,7 @@ const NetworkUpgradeSummary = ({ name }: NetworkUpgradeSummaryProps) => { hour: "numeric", minute: "numeric", second: "numeric", - }) + }).format(date) setFormattedUTC(`${formattedDate} +UTC`) }, [dateTimeAsString, locale]) @@ -57,7 +55,7 @@ const NetworkUpgradeSummary = ({ name }: NetworkUpgradeSummaryProps) => { {t(translationKey)}:{" "} - {new Intl.NumberFormat(localeForStatsBoxNumbers).format(number)} + {numberFormat(locale).format(number)} ) @@ -93,7 +91,7 @@ const NetworkUpgradeSummary = ({ name }: NetworkUpgradeSummaryProps) => { {t("page-history:page-history-eth-price")}:{" "} - {new Intl.NumberFormat(localeForStatsBoxNumbers, { + {numberFormat(locale, { style: "currency", currency: "USD", }).format(ethPriceInUSD)} diff --git a/src/components/Homepage/KPISection.tsx b/src/components/Homepage/KPISection.tsx index 3b9a1d93b7b..7612c815475 100644 --- a/src/components/Homepage/KPISection.tsx +++ b/src/components/Homepage/KPISection.tsx @@ -7,6 +7,7 @@ import { useIntersectionObserver } from "usehooks-ts" import { Section, SectionHeader, SectionTag } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" type KPISectionProps = { accountHolders: number | null @@ -133,7 +134,7 @@ function formatNumber(value: number): string { return `${Math.round(value / 1_000_000)}M` } if (value >= 1_000) { - return new Intl.NumberFormat("en-US").format(value) + return numberFormat("en-US").format(value) } return value.toString() } diff --git a/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx b/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx index 5af5d6184bc..48d2e000e5f 100644 --- a/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx +++ b/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx @@ -16,6 +16,7 @@ import { TableCell, TableHead } from "@/components/ui/table" import { cn } from "@/lib/utils/cn" import { trackCustomEvent } from "@/lib/utils/matomo" +import { numberFormat } from "@/lib/utils/numbers" export const useNetworkColumns: ColumnDef[] = [ { @@ -81,13 +82,10 @@ export const useNetworkColumns: ColumnDef[] = [ {row.original.txCosts ? ( <> $ - {(row.original.txCosts || 0).toLocaleString( - meta.locale as Lang, - { - minimumFractionDigits: 2, - maximumFractionDigits: 3, - } - )} + {numberFormat(meta.locale as Lang, { + minimumFractionDigits: 2, + maximumFractionDigits: 3, + }).format(row.original.txCosts || 0)} ) : (

-

@@ -99,7 +97,7 @@ export const useNetworkColumns: ColumnDef[] = [

- {new Intl.NumberFormat(meta.locale as Lang, { + {numberFormat(meta.locale as Lang, { style: "currency", currency: "USD", notation: "compact", @@ -172,10 +170,10 @@ export const useNetworkColumns: ColumnDef[] = [ {row.original.txCosts ? (

$ - {row.original.txCosts.toLocaleString(meta.locale as Lang, { + {numberFormat(meta.locale as Lang, { minimumFractionDigits: 2, maximumFractionDigits: 3, - })} + }).format(row.original.txCosts)}

) : (

-

@@ -224,7 +222,7 @@ export const useNetworkColumns: ColumnDef[] = [ )} >

- {new Intl.NumberFormat(meta.locale as Lang, { + {numberFormat(meta.locale as Lang, { style: "currency", currency: "USD", notation: "compact", diff --git a/src/components/LocaleDateTime.tsx b/src/components/LocaleDateTime.tsx index 2ab4c7e50b7..acd34bd89e5 100644 --- a/src/components/LocaleDateTime.tsx +++ b/src/components/LocaleDateTime.tsx @@ -1,5 +1,7 @@ import { useLocale } from "next-intl" +import { dateTimeFormat } from "@/lib/utils/date" + type LocaleDateTimeProps = { utcDateTime: string hideDate?: boolean @@ -45,7 +47,7 @@ const LocaleDateTime = ({ } return ( ) } diff --git a/src/components/Quiz/QuizWidget/QuizSummary.tsx b/src/components/Quiz/QuizWidget/QuizSummary.tsx index c223a879963..961199a3fbe 100644 --- a/src/components/Quiz/QuizWidget/QuizSummary.tsx +++ b/src/components/Quiz/QuizWidget/QuizSummary.tsx @@ -3,7 +3,7 @@ import { useLocale } from "next-intl" import { HStack, VStack } from "@/components/ui/flex" import { cn } from "@/lib/utils/cn" -import { numberToPercent } from "@/lib/utils/numberToPercent" +import { numberToPercent } from "@/lib/utils/numbers" import { screens } from "@/lib/utils/screen" import { useMediaQuery } from "@/hooks/useMediaQuery" diff --git a/src/components/Quiz/QuizzesStats.tsx b/src/components/Quiz/QuizzesStats.tsx index 4f3d6c24af9..d89e73b4811 100644 --- a/src/components/Quiz/QuizzesStats.tsx +++ b/src/components/Quiz/QuizzesStats.tsx @@ -61,7 +61,7 @@ const QuizzesStats = ({ formattedCollectiveQuestionsAnswered, formattedCollectiveAverageScore, formattedCollectiveRetryRate, - } = getFormattedStats(locale!, averageScoresArray) + } = getFormattedStats(locale, averageScoresArray) return (

diff --git a/src/components/Quiz/utils.ts b/src/components/Quiz/utils.ts index 7065b2874dc..3d6f6779c5e 100644 --- a/src/components/Quiz/utils.ts +++ b/src/components/Quiz/utils.ts @@ -1,11 +1,10 @@ import type { CompletedQuizzes, - Lang, QuizShareStats, QuizzesSection, } from "@/lib/types" -import { getLocaleForNumberFormat } from "@/lib/utils/translations" +import { numberFormat } from "@/lib/utils/numbers" import allQuizzesData, { ethereumBasicsQuizzes, @@ -57,23 +56,21 @@ export const shareOnTwitter = ({ score, total }: QuizShareStats): void => { const mean = (values: number[]) => values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0 -export const getFormattedStats = (language, average) => { - const localeForNumbers = getLocaleForNumberFormat(language as Lang) - +export const getFormattedStats = (locale: string, valueSet: number[]) => { // Initialize number and percent formatters - const numberFormatter = new Intl.NumberFormat(localeForNumbers, { + const numberFormatter = numberFormat(locale, { style: "decimal", minimumSignificantDigits: 1, maximumSignificantDigits: 3, }) - const percentFormatter = new Intl.NumberFormat(localeForNumbers, { + const percentFormatter = numberFormat(locale, { style: "percent", minimumSignificantDigits: 1, maximumSignificantDigits: 3, }) - const computedAverage = average.length > 0 ? mean(average) : 0 + const computedAverage = valueSet.length > 0 ? mean(valueSet) : 0 // Convert collective stats to fraction for percentage format const normalizedCollectiveAverageScore = TOTAL_QUIZ_AVERAGE_SCORE / 100 diff --git a/src/components/RadialChart/index.tsx b/src/components/RadialChart/index.tsx index 53a4d782050..08375a78243 100644 --- a/src/components/RadialChart/index.tsx +++ b/src/components/RadialChart/index.tsx @@ -6,7 +6,7 @@ import { useLocale } from "next-intl" import { PolarAngleAxis, RadialBar, RadialBarChart } from "recharts" import { cn } from "@/lib/utils/cn" -import { isValidDate } from "@/lib/utils/date" +import { dateTimeFormat, isValidDate } from "@/lib/utils/date" import Tooltip from "../Tooltip" import Link from "../ui/Link" @@ -62,7 +62,7 @@ const RadialChart = ({ const lastUpdatedDisplay = lastUpdated && isValidDate(lastUpdated) - ? new Intl.DateTimeFormat(locale, { + ? dateTimeFormat(locale, { dateStyle: "medium", }).format(new Date(lastUpdated)) : "" diff --git a/src/components/Simulator/WalletHome/TokenBalanceItem.tsx b/src/components/Simulator/WalletHome/TokenBalanceItem.tsx index cbf4c813b64..2552d18fd5f 100644 --- a/src/components/Simulator/WalletHome/TokenBalanceItem.tsx +++ b/src/components/Simulator/WalletHome/TokenBalanceItem.tsx @@ -1,5 +1,7 @@ import { Flex } from "@/components/ui/flex" +import { numberFormat } from "@/lib/utils/numbers" + import { getMaxFractionDigitsUsd } from "../utils" import { TokenBalance } from "./interfaces" @@ -10,13 +12,13 @@ type TokenBalanceItemProps = { export const TokenBalanceItem = ({ item }: TokenBalanceItemProps) => { const { name, ticker, amount, usdConversion, Icon } = item const usdAmount = amount * usdConversion - const usdValue = Intl.NumberFormat("en-US", { + const usdValue = numberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), }).format(usdAmount) - const tokenAmount = Intl.NumberFormat("en", { + const tokenAmount = numberFormat("en", { maximumFractionDigits: 5, }).format(amount) return ( diff --git a/src/components/Simulator/WalletHome/WalletBalance.tsx b/src/components/Simulator/WalletHome/WalletBalance.tsx index b5031c88136..30f499066dd 100644 --- a/src/components/Simulator/WalletHome/WalletBalance.tsx +++ b/src/components/Simulator/WalletHome/WalletBalance.tsx @@ -2,6 +2,8 @@ import React from "react" import { Flex } from "@/components/ui/flex" +import { numberFormat } from "@/lib/utils/numbers" + import { getMaxFractionDigitsUsd } from "../utils" import { AddressPill } from "./AddressPill" @@ -14,7 +16,7 @@ export const WalletBalance = ({ usdAmount = 0 }: WalletBalanceProps) => (

Your total

- {Intl.NumberFormat("en-US", { + {numberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", diff --git a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx index dc5d4640e10..24dafb07d1b 100644 --- a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx +++ b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx @@ -4,6 +4,8 @@ import { AnimatePresence, motion } from "motion/react" import type { SimulatorNavProps } from "@/lib/types" +import { numberFormat } from "@/lib/utils/numbers" + import { getMaxFractionDigitsUsd } from "../../utils" import { WalletHome } from "../../WalletHome" import type { TokenBalance } from "../../WalletHome/interfaces" @@ -60,11 +62,11 @@ export const ReceivedEther = ({ const tokenBalances = received ? tokensWithEthBalance : defaultTokenBalances - const displayEth: string = new Intl.NumberFormat("en", { + const displayEth: string = numberFormat("en", { maximumFractionDigits: 5, }).format(ethReceiveAmount) const usdReceiveAmount = ethReceiveAmount * ethPrice - const displayUsd: string = new Intl.NumberFormat("en", { + const displayUsd: string = numberFormat("en", { style: "currency", currency: "USD", notation: "compact", diff --git a/src/components/Simulator/screens/SendReceive/SendEther.tsx b/src/components/Simulator/screens/SendReceive/SendEther.tsx index 0f53c24516b..cd87450f7df 100644 --- a/src/components/Simulator/screens/SendReceive/SendEther.tsx +++ b/src/components/Simulator/screens/SendReceive/SendEther.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/buttons/Button" import { Flex, HStack } from "@/components/ui/flex" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import { EthTokenIcon } from "../../icons" import { NotificationPopover } from "../../NotificationPopover" @@ -21,7 +22,7 @@ export const SendEther = ({ setChosenAmount, }: SendEtherProps) => { const formatDollars = (amount: number): string => - new Intl.NumberFormat("en-US", { + numberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", @@ -29,7 +30,7 @@ export const SendEther = ({ const usdAmount = formatDollars(ethPrice * ethBalance) - const ethAmount = new Intl.NumberFormat("en", { + const ethAmount = numberFormat("en", { maximumFractionDigits: 5, }).format(ethBalance) @@ -48,7 +49,7 @@ export const SendEther = ({ if (amount === maxUsdAmount) return "Max" return formatDollars(amount) } - const formatChosenAmount = new Intl.NumberFormat("en", { + const formatChosenAmount = numberFormat("en", { style: "currency", currency: "USD", notation: "compact", diff --git a/src/components/Simulator/screens/SendReceive/SendSummary.tsx b/src/components/Simulator/screens/SendReceive/SendSummary.tsx index 1874aea4a5c..8a541cfb828 100644 --- a/src/components/Simulator/screens/SendReceive/SendSummary.tsx +++ b/src/components/Simulator/screens/SendReceive/SendSummary.tsx @@ -3,6 +3,7 @@ import React from "react" import { Flex } from "@/components/ui/flex" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import { ETH_TRANSFER_FEE } from "../../constants" import { getMaxFractionDigitsUsd } from "../../utils" @@ -19,9 +20,9 @@ export const SendSummary = ({ recipient, }: SendSummaryProps) => { const formatEth = (amount: number): string => - new Intl.NumberFormat("en", { maximumFractionDigits: 5 }).format(amount) + numberFormat("en", { maximumFractionDigits: 5 }).format(amount) - const formatChosenAmount = new Intl.NumberFormat("en", { + const formatChosenAmount = numberFormat("en", { style: "currency", currency: "USD", notation: "compact", @@ -63,7 +64,7 @@ export const SendSummary = ({

Network fees

- {Intl.NumberFormat("en", { + {numberFormat("en", { maximumFractionDigits: getMaxFractionDigitsUsd(usdFee), style: "currency", currency: "USD", @@ -71,7 +72,7 @@ export const SendSummary = ({ }).format(usdFee)} ( - {Intl.NumberFormat("en", { + {numberFormat("en", { maximumFractionDigits: 6, }).format(ETH_TRANSFER_FEE)}{" "} ETH) diff --git a/src/components/Simulator/screens/SendReceive/Success.tsx b/src/components/Simulator/screens/SendReceive/Success.tsx index e513f119110..495d2384a9c 100644 --- a/src/components/Simulator/screens/SendReceive/Success.tsx +++ b/src/components/Simulator/screens/SendReceive/Success.tsx @@ -6,6 +6,7 @@ import { Flex, VStack } from "@/components/ui/flex" import { Spinner } from "@/components/ui/spinner" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import { getMaxFractionDigitsUsd } from "../../utils" import { WalletHome } from "../../WalletHome" @@ -31,14 +32,14 @@ export const Success = ({ const usdAmount = sentEthAmount * ethPrice - const usdValue = new Intl.NumberFormat("en", { + const usdValue = numberFormat("en", { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), }).format(usdAmount) - const sentEthValue = new Intl.NumberFormat("en", { + const sentEthValue = numberFormat("en", { maximumFractionDigits: 5, }).format(sentEthAmount) diff --git a/src/components/Staking/StakingStatsBox.tsx b/src/components/Staking/StakingStatsBox.tsx index 2aa803b4e9e..3bacac653f5 100644 --- a/src/components/Staking/StakingStatsBox.tsx +++ b/src/components/Staking/StakingStatsBox.tsx @@ -1,12 +1,12 @@ import { Info } from "lucide-react" import { useLocale } from "next-intl" -import type { ChildOnlyProp, Lang, StakingStatsData } from "@/lib/types" +import type { ChildOnlyProp, StakingStatsData } from "@/lib/types" import Tooltip from "@/components/Tooltip" import { Flex, VStack } from "@/components/ui/flex" -import { getLocaleForNumberFormat } from "@/lib/utils/translations" +import { numberFormat } from "@/lib/utils/numbers" import InlineLink from "../ui/Link" @@ -43,16 +43,12 @@ const StakingStatsBox = ({ data }: StakingStatsBoxProps) => { const locale = useLocale() const { t } = useTranslation("page-staking") - const localeForStatsBoxNumbers = getLocaleForNumberFormat(locale! as Lang) - // Helper functions const formatInteger = (amount: number): string => - amount - ? new Intl.NumberFormat(localeForStatsBoxNumbers).format(amount) - : "—" + amount ? numberFormat(locale).format(amount) : "—" const formatPercentage = (amount: number): string => - new Intl.NumberFormat(localeForStatsBoxNumbers, { + numberFormat(locale, { style: "percent", minimumSignificantDigits: 2, maximumSignificantDigits: 2, diff --git a/src/lib/types.ts b/src/lib/types.ts index d103d883653..0a92481106e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -166,7 +166,6 @@ export type I18nLocale = { name: string localName: string langDir: Direction - dateFormat: string /** * @property forceLocalName - Optional flag to indicate that the local name should be used instead of the fallback from `Intl.DisplayName`. * Fallback used when locale language name matches English name. diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts index 82d53ca18df..851ac1597fa 100644 --- a/src/lib/utils/date.ts +++ b/src/lib/utils/date.ts @@ -1,4 +1,35 @@ import { DEFAULT_LOCALE } from "../constants" +import type { Lang } from "../types" + +/** + * A wrapper for Intl.DateTimeFormat that enforces Web3 date standards. + * - Forces the Gregorian calendar universally. + * - Arabic ('ar') and standard locales default to Western numerals (1, 2, 3). + * - Urdu ('ur') defaults to Extended Arabic numerals (۱, ۲, ۳). + */ +export function dateTimeFormat( + locale: string, + options?: Intl.DateTimeFormatOptions +): Intl.DateTimeFormat { + let numberingSystem = options?.numberingSystem + + if (!numberingSystem) { + if (locale === "ur") { + numberingSystem = "arabext" // Native Urdu numerals + } else { + numberingSystem = "latn" // Western numerals for Arabic, Indic, etc. + } + } + + const finalOptions: Intl.DateTimeFormatOptions = { + // ALWAYS force Gregorian for tech/Web3 consistency + calendar: "gregory", + ...options, + ...(numberingSystem && { numberingSystem }), + } + + return new Intl.DateTimeFormat(locale, finalOptions) +} export const dateToString = (published: Date | string) => new globalThis.Date(published).toISOString().split("T")[0] @@ -27,12 +58,12 @@ export const formatDate = ( if (/^\d{4}$/.test(date)) { return date } - return new Date(date).toLocaleDateString(locale, { + return dateTimeFormat(locale, { month: "long", day: "numeric", year: "numeric", ...options, - }) + }).format(new Date(date)) } export const isDateReached = (date: string) => { @@ -47,20 +78,25 @@ export const formatDateRange = ( locale: string = DEFAULT_LOCALE, options?: Intl.DateTimeFormatOptions ) => - new Intl.DateTimeFormat(locale, { + dateTimeFormat(locale, { month: "short", day: "numeric", ...options, }).formatRange(new Date(start), new Date(end || start)) export const getLocaleYear = ( - locale: Intl.LocalesArgument = "en-US", + locale: string = "en-US", date?: ConstructorParameters[0] ) => - new Intl.DateTimeFormat(locale, { year: "numeric" }).format( + dateTimeFormat(locale, { year: "numeric" }).format( date ? new Date(date) : new Date() ) +export const getLocaleFormattedDate = (locale: Lang, date: string) => { + const walletLastUpdatedDate = new Date(date) + return dateTimeFormat(locale).format(walletLastUpdatedDate) +} + /** * Get ISO week number for a given date * Used as seed for deterministic weekly rotation diff --git a/src/lib/utils/numberToPercent.ts b/src/lib/utils/numberToPercent.ts deleted file mode 100644 index 9c2774fbbf8..00000000000 --- a/src/lib/utils/numberToPercent.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Lang } from "@/lib/types" - -import { getLocaleForNumberFormat } from "@/lib/utils/translations" - -import { DEFAULT_LOCALE } from "../constants" - -export const numberToPercent = ( - num: number, - locale: string | Lang = DEFAULT_LOCALE -): string => - new Intl.NumberFormat(getLocaleForNumberFormat(locale as Lang), { - style: "percent", - maximumFractionDigits: 0, - }).format(num) diff --git a/src/lib/utils/numbers.ts b/src/lib/utils/numbers.ts index ac48d4f33c5..48a108611a8 100644 --- a/src/lib/utils/numbers.ts +++ b/src/lib/utils/numbers.ts @@ -1,5 +1,39 @@ +/** + * A wrapper for Intl.NumberFormat that enforces Web3 numeral standards. + * - Arabic ('ar') defaults to Western Arabic numerals (1, 2, 3). + * - Urdu ('ur') defaults to Extended Arabic numerals (۱, ۲, ۳). + * - All other locales default to 'latn' to prevent browser-specific quirks. + */ +export function numberFormat( + locale: string, + options?: Intl.NumberFormatOptions +): Intl.NumberFormat { + // If numberingSystem explicitly passed, respect it. + // Otherwise, apply our localization rules. + let numberingSystem = options?.numberingSystem + + if (!numberingSystem) { + if (locale === "ur") { + // Force Extended Arabic numerals for Urdu + numberingSystem = "arabext" + } else { + // Force Western Arabic numerals ('latn') for Arabic and all other locales + // to override browser defaults that might try to use native scripts. + numberingSystem = "latn" + } + } + + // Merge the resolved numbering system into the options + const finalOptions: Intl.NumberFormatOptions = { + ...options, + ...(numberingSystem && { numberingSystem }), + } + + return new Intl.NumberFormat(locale, finalOptions) +} + export const formatLargeUSD = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { style: "currency", currency: "USD", notation: "compact", @@ -9,7 +43,7 @@ export const formatLargeUSD = (value: number, locale: string): string => { } export const formatSmallUSD = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { style: "currency", currency: "USD", notation: "compact", @@ -19,7 +53,7 @@ export const formatSmallUSD = (value: number, locale: string): string => { } export const formatLargeNumber = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { notation: "compact", minimumSignificantDigits: 3, maximumSignificantDigits: 4, @@ -27,10 +61,16 @@ export const formatLargeNumber = (value: number, locale: string): string => { } export const formatPriceUSD = (value: number, locale: string): string => { - return new Intl.NumberFormat(locale, { + return numberFormat(locale, { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(value) } + +export const numberToPercent = (num: number, locale: string): string => + numberFormat(locale, { + style: "percent", + maximumFractionDigits: 0, + }).format(num) diff --git a/src/lib/utils/time.ts b/src/lib/utils/time.ts index 0eadcd13ad6..2394175c855 100644 --- a/src/lib/utils/time.ts +++ b/src/lib/utils/time.ts @@ -1,5 +1,7 @@ import { Lang } from "../types" +import { dateTimeFormat } from "./date" + export const getLocaleTimestamp = ( locale: Lang, timestamp: string, @@ -13,11 +15,5 @@ export const getLocaleTimestamp = ( day: "numeric", } as Intl.DateTimeFormatOptions) const date = new Date(timestamp) - return new Intl.DateTimeFormat(locale, opts).format(date) -} - -export const getLocaleFormattedDate = (locale: Lang, date: string) => { - const walletLastUpdatedDate = new Date(date) - - return new Intl.DateTimeFormat(locale).format(walletLastUpdatedDate) + return dateTimeFormat(locale, opts).format(date) } diff --git a/src/lib/utils/translations.ts b/src/lib/utils/translations.ts index 13abc647a80..dc673b9f80b 100644 --- a/src/lib/utils/translations.ts +++ b/src/lib/utils/translations.ts @@ -138,15 +138,6 @@ export const filterRealLocales = (locales: string[] | undefined) => { return locales?.filter((locale) => locale !== FAKE_LOCALE) || [] } -export const isLocaleValidISO639_1 = (locale: string) => { - return i18nConfig.find((language) => language.code === locale)?.validISO639_1 -} - -// Returns the locale to use for number formatting -// Note: Previously had special case for Farsi (fa) to use Hindu-Arabic numerals, -// but Farsi is no longer in the active locale set -export const getLocaleForNumberFormat = (locale: Lang): Lang => locale - export const isLang = (lang: string) => { return i18nConfig.map((language) => language.code).includes(lang) }