From df9044652cc35d9ec3e7b1b8e818bd56960e9413 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 14 Apr 2026 15:28:39 +0200 Subject: [PATCH 001/109] feat: replace homepage with 2026 redesign (variant B winner) Replace the old homepage with the Homepage2026 component that won the A/B/C test (PR #17261). The components were already in the codebase from the test; this wires them up as the permanent homepage. Key changes: - page.tsx simplified to render Homepage2026 directly - Hero heading updated to "The internet that belongs to you" - Persona modal restructured: Beginners, Explorers, Builders (+ Enterprise on mobile) - Privacy carousel slide replaces DeFi savings slide - Trust section copy updated, logos removed per final Figma design - Simulator section moved before Feature Cards per Figma order --- app/[locale]/page.tsx | 931 +------------------- src/components/Hero/HomeHero2026/index.tsx | 2 +- src/components/Homepage/FeatureCards.tsx | 2 +- src/components/Homepage/Homepage2026.tsx | 8 +- src/components/Homepage/PersonaModalCTA.tsx | 190 ++-- src/components/Homepage/SavingsCarousel.tsx | 70 +- src/components/Homepage/TrustLogos.tsx | 67 +- 7 files changed, 180 insertions(+), 1090 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index be461138fb1..d5f4a42eb4c 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,98 +1,17 @@ -import { Fragment } from "react" -import { Info } from "lucide-react" import { notFound } from "next/navigation" import { getTranslations, setRequestLocale } from "next-intl/server" -import type { - AllHomepageActivityData, - CommunityBlog, - PageParams, - ValuesPairing, -} from "@/lib/types" -import { CodeExample } from "@/lib/interfaces" +import type { PageParams } from "@/lib/types" -import ActivityStats from "@/components/ActivityStats" -import { ChevronNext } from "@/components/Chevron" -import HomeHero from "@/components/Hero/HomeHero" -import BentoCard from "@/components/Homepage/BentoCard" -import HomepageSectionImage from "@/components/Homepage/HomepageSectionImage" -import { getBentoBoxItems } from "@/components/Homepage/utils" -import BlockHeap from "@/components/icons/block-heap.svg" -import BuildAppsIcon from "@/components/icons/build-apps.svg" -import Discord from "@/components/icons/discord.svg" -import EthGlyphIcon from "@/components/icons/eth-glyph.svg" -import EthTokenIcon from "@/components/icons/eth-token.svg" -import PickWalletIcon from "@/components/icons/eth-wallet.svg" -import Github from "@/components/icons/github.svg" -import TryAppsIcon from "@/components/icons/phone-homescreen.svg" -import RoadmapSign from "@/components/icons/roadmap-sign.svg" -import Twitter from "@/components/icons/twitter.svg" -import Whitepaper from "@/components/icons/whitepaper.svg" -import { Image } from "@/components/Image" -import CardImage from "@/components/Image/CardImage" -import IntersectionObserverReveal from "@/components/IntersectionObserverReveal" -import MainArticle from "@/components/MainArticle" -import Tooltip from "@/components/Tooltip" -import { ButtonLink } from "@/components/ui/buttons/Button" -import SvgButtonLink, { - type SvgButtonLinkProps, -} from "@/components/ui/buttons/SvgButtonLink" -import { - Card, - CardBanner, - CardContent, - CardParagraph, - CardTitle, -} from "@/components/ui/card" -import InlineLink from "@/components/ui/Link" -import Link from "@/components/ui/Link" -import { - Section, - SectionBanner, - SectionContent, - SectionHeader, - SectionTag, -} from "@/components/ui/section" +import Homepage2026 from "@/components/Homepage/Homepage2026" -import { parseAppsOfTheWeek } from "@/lib/utils/apps" -import { cn } from "@/lib/utils/cn" -import { formatDateRange } from "@/lib/utils/date" -import { getDirection } from "@/lib/utils/direction" -import { getLocalizedDescription } from "@/lib/utils/i18n-descriptions" import { getMetadata } from "@/lib/utils/metadata" -import { formatPriceUSD } from "@/lib/utils/numbers" -import { polishRSSList } from "@/lib/utils/rss" -import { - BLOGS_WITHOUT_FEED, - DEFAULT_LOCALE, - ENTERPRISE_ETHEREUM_URL, - GITHUB_REPO_URL, - LOCALES_CODES, - RSS_DISPLAY_COUNT, -} from "@/lib/constants" +import { DEFAULT_LOCALE, LOCALES_CODES } from "@/lib/constants" -import { - AppsHighlight, - BentoCardSwiper, - CodeExamples, - RecentPostsSwiper, - ValuesMarquee, -} from "./_components/HomepageLazyImports" import IndexPageJsonLD from "./page-jsonld" -import { getActivity } from "./utils" -import { - getAppsData, - getAttestantPosts, - getBeaconchainData, - getEthPrice, - getEventsData, - getGrowThePieData, - getRSSData, - getTotalValueLockedData, -} from "@/lib/data" -import EventFallback from "@/public/images/events/event-placeholder.png" +import { getAccountHolders, getGrowThePieData } from "@/lib/data" const Page = async (props: { params: Promise }) => { const params = await props.params @@ -102,839 +21,29 @@ const Page = async (props: { params: Promise }) => { setRequestLocale(locale) - const t = await getTranslations("page-index") - const tCommon = await getTranslations("common") - const appDescriptions = await getTranslations("page-app-descriptions") - const { direction: dir, isRtl } = getDirection(locale) - - // Fetch data using the new data-layer functions (already cached) - const [ - ethPrice, - beaconchainData, - totalValueLocked, - growThePieData, - attestantPosts, - rssData, - appsData, - eventsData, - ] = await Promise.all([ - getEthPrice(), - getBeaconchainData(), - getTotalValueLockedData(), - getGrowThePieData(), - getAttestantPosts(), - getRSSData(), - getAppsData(), - getEventsData(), + const [accountHoldersData, growThePieData] = await Promise.all([ + getAccountHolders().catch(() => null), + getGrowThePieData().catch(() => null), ]) - // Handle null cases - throw error if required data is missing - if (!ethPrice) { - throw new Error("Failed to fetch ETH price data") - } - if (!beaconchainData) { - throw new Error("Failed to fetch Beaconchain data") - } - if (!totalValueLocked) { - throw new Error("Failed to fetch total value locked data") - } - if (!growThePieData) { - throw new Error("Failed to fetch GrowThePie data") - } - if (!appsData) { - throw new Error("Failed to fetch apps data") - } - - // RSS feeds - graceful degradation: use what's available if we have enough items - const rssFeeds = rssData ?? [] - const attestantFeed = attestantPosts ?? [] - const totalRssItems = - rssFeeds.reduce((sum, feed) => sum + feed.length, 0) + attestantFeed.length - - if (totalRssItems < RSS_DISPLAY_COUNT) { - throw new Error( - `Insufficient RSS data: need at least ${RSS_DISPLAY_COUNT} items` - ) - } - - // Extract totalEthStaked from beaconchainData - const { totalEthStaked } = beaconchainData - - // Events - use empty array as fallback - const upcomingEvents = (eventsData ?? []).slice(0, 3) - - const appsOfTheWeek = parseAppsOfTheWeek(appsData) - - const bentoItems = await getBentoBoxItems() - - const ethPriceHasError = "error" in ethPrice - - const price = ethPriceHasError - ? t("loading-error-refresh") - : formatPriceUSD(ethPrice.value, locale) - - const eventCategory = `Homepage - ${locale}` - - const subHeroCTAs = [ - { - label: t("page-index-cta-wallet-label"), - description: t("page-index-cta-wallet-description"), - href: "/wallets/find-wallet/", - Svg: PickWalletIcon, - className: "text-primary hover:text-primary-hover", - eventName: "find_wallet", - }, - { - label: t("page-index-cta-get-eth-label"), - description: t("page-index-cta-get-eth-description"), - href: "/get-eth/", - Svg: EthTokenIcon, - className: "text-accent-a hover:text-accent-a-hover", - eventName: "get_eth", - }, - { - label: t("page-index-cta-dapps-label"), - description: t("page-index-cta-dapps-description"), - href: "/apps/", - Svg: TryAppsIcon, - className: cn( - "text-accent-c hover:text-accent-c-hover", - isRtl && "[&_svg]:-scale-x-100" - ), - eventName: "try_apps", - }, - { - label: t("page-index-cta-build-apps-label"), - description: t("page-index-cta-build-apps-description"), - href: "/developers/", - Svg: BuildAppsIcon, - className: "text-accent-b hover:text-accent-b-hover", - eventName: "start_building", - }, - ] - - const popularTopics = [ - { - label: t("page-index-popular-topics-ethereum"), - Svg: EthTokenIcon, - href: "/what-is-ethereum/", - eventName: "ethereum", - }, - { - label: t("page-index-popular-topics-wallets"), - Svg: PickWalletIcon, - href: "/wallets/", - eventName: "wallets", - }, - { - label: t("page-index-popular-topics-start"), - Svg: BlockHeap, - href: "/guides/", - eventName: "start guides", - }, - { - label: t("page-index-popular-topics-whitepaper"), - Svg: Whitepaper, - className: cn(isRtl && "[&_div_div:has(svg)]:-scale-x-100"), - href: "/whitepaper/", - eventName: "whitepaper", - }, - { - label: t("page-index-popular-topics-roadmap"), - Svg: RoadmapSign, - className: cn(isRtl && "[&_div_div:has(svg)]:-scale-x-100 "), - href: "/roadmap/", - eventName: "roadmap", - }, - ] - - const valuesPairings: ValuesPairing[] = [ - { - legacy: { - label: t("page-index-values-ownership-legacy-label"), - content: [ - t("page-index-values-ownership-legacy-content-0"), - t("page-index-values-ownership-legacy-content-1"), - ], - }, - ethereum: { - label: t("page-index-values-ownership-ethereum-label"), - content: [t("page-index-values-ownership-ethereum-content-0")], - }, - }, - { - legacy: { - label: t("page-index-values-fairness-legacy-label"), - content: [t("page-index-values-fairness-legacy-content-0")], - }, - ethereum: { - label: t("page-index-values-fairness-ethereum-label"), - content: [t("page-index-values-fairness-ethereum-content-0")], - }, - }, - { - legacy: { - label: t("page-index-values-privacy-legacy-label"), - content: [ - t("page-index-values-privacy-legacy-content-0"), - t("page-index-values-privacy-legacy-content-1"), - ], - }, - ethereum: { - label: t("page-index-values-privacy-ethereum-label"), - content: [t("page-index-values-privacy-ethereum-content-0")], - }, - }, - { - legacy: { - label: t("page-index-values-integration-legacy-label"), - content: [t("page-index-values-integration-legacy-content-0")], - }, - ethereum: { - label: t("page-index-values-integration-ethereum-label"), - content: [t("page-index-values-integration-ethereum-content-0")], - }, - }, - { - legacy: { - label: t("page-index-values-decentralization-legacy-label"), - content: [t("page-index-values-decentralization-legacy-content-0")], - }, - ethereum: { - label: t("page-index-values-decentralization-ethereum-label"), - content: [t("page-index-values-decentralization-ethereum-content-0")], - }, - }, - { - legacy: { - label: t("page-index-values-censorship-legacy-label"), - content: [t("page-index-values-censorship-legacy-content-0")], - }, - ethereum: { - label: t("page-index-values-censorship-ethereum-label"), - content: [ - t("page-index-values-censorship-ethereum-content-0"), - t("page-index-values-censorship-ethereum-content-1"), - ], - }, - }, - { - legacy: { - label: t("page-index-values-open-legacy-label"), - content: [t("page-index-values-open-legacy-content-0")], - }, - ethereum: { - label: t("page-index-values-open-ethereum-label"), - content: [t("page-index-values-open-ethereum-content-0")], - }, - }, - ] - - const codeExamples: CodeExample[] = [ - { - title: t("page-index-developers-code-example-title-0"), - description: t("page-index-developers-code-example-description-0"), - codeLanguage: "language-solidity", - codeUrl: "/code-examples/SimpleWallet.sol", - eventName: "bank", - }, - { - title: t("page-index-developers-code-example-title-1"), - description: t("page-index-developers-code-example-description-1"), - codeLanguage: "language-solidity", - codeUrl: "/code-examples/SimpleToken.sol", - eventName: "token", - }, - { - title: t("page-index-developers-code-example-title-2"), - description: t("page-index-developers-code-example-description-2"), - codeLanguage: "language-javascript", - codeUrl: "/code-examples/CreateWallet.js", - eventName: "wallet", - }, - { - title: t("page-index-developers-code-example-title-3"), - description: t("page-index-developers-code-example-description-3"), - codeLanguage: "language-solidity", - codeUrl: "/code-examples/SimpleDomainRegistry.sol", - eventName: "dns", - }, - ] - - const joinActions = [ - { - Svg: EthGlyphIcon, - label: t("page-index-join-action-contribute-label"), - href: "/contributing/", - className: "text-accent-c hover:text-accent-c-hover", - description: t("page-index-join-action-contribute-description"), - eventName: "contribute", - }, - { - Svg: Github, - label: "GitHub", - href: GITHUB_REPO_URL, - className: "text-accent-a hover:text-accent-a-hover", - description: t("page-index-join-action-github-description"), - eventName: "GitHub", - }, - { - Svg: Discord, - label: "Discord", - href: "https://discord.gg/ethereum-org", - className: "text-primary hover:text-primary-hover", - description: t("page-index-join-action-discord-description"), - eventName: "Discord", - }, - { - Svg: Twitter, - label: "X", - href: "https://x.com/EthDotOrg", - className: "text-accent-b hover:text-accent-b-hover", - description: t("page-index-join-action-twitter-description"), - eventName: "Twitter", - }, - ] - - const metricResults: AllHomepageActivityData = { - ethPrice, - totalEthStaked, - totalValueLocked, - txCount: growThePieData.txCount, - txCostsMedianUsd: growThePieData.txCostsMedianUsd, - } - const metrics = await getActivity(metricResults, locale) + const accountHolders = + accountHoldersData && "value" in accountHoldersData + ? accountHoldersData.value + : null - // RSS feed items - // polishRSSList expects RSSItem[][], so wrap attestantFeed in an array - const polishedRssItems = polishRSSList([attestantFeed, ...rssFeeds], locale) - const rssItems = polishedRssItems.slice(0, RSS_DISPLAY_COUNT) - - const blogLinks = polishedRssItems.map(({ source, sourceUrl }) => ({ - name: source, - href: sourceUrl, - })) as CommunityBlog[] - blogLinks.push(...BLOGS_WITHOUT_FEED) + const transactionsToday = + growThePieData?.txCount && "value" in growThePieData.txCount + ? growThePieData.txCount.value + : null return ( <> - - -
-
- {subHeroCTAs.map( - ({ label, description, href, className, Svg }, idx) => { - const Link = ( - props: Omit< - SvgButtonLinkProps, - "Svg" | "href" | "label" | "children" - > - ) => ( - -

{description}

-
- ) - return ( - - - - - ) - } - )} -
- - {/* What is Ethereum */} -
- - - - - - {t("page-index-network-tag")} - - {t("page-index-what-is-ethereum-title")} - -
-

{t("page-index-what-is-ethereum-description-1")}

-

{t("page-index-what-is-ethereum-description-2")}

-
-
- - {t("page-index-what-is-ethereum-action")} - -
- - {/* Popular topics */} -
-

- {t("page-index-popular-topics-header")} -

-
- {popularTopics - .filter((topic) => topic.href !== "/what-is-ethereum/") - .map(({ label, Svg, href, eventName, className }) => ( - :first-child]:flex-row", - className - )} - customEventOptions={{ - eventCategory, - eventAction: "popular topics", - eventName, - }} - > -

- {label} -

-
- ))} -
-
-
-
- - {/* Use Cases - A new way to use the internet */} -
-
-
- {t("page-index-use-cases-tag")} -
-

- {t("page-index-bento-header")} -

-
- - {/* Mobile - dynamic / lazy loaded */} - - - {/* Desktop */} - {bentoItems.map(({ className, ...item }) => ( - - ))} -
- - {/* What is ETH */} -
- - - - - - {t("page-index-token-tag")} - - {t("page-index-what-is-ether-title")} - -
-

{t("page-index-what-is-ether-description-1")}

-

{t("page-index-what-is-ether-description-2")}

-
-
-
- {price} -
-
- {tCommon("eth-current-price")} - - {tCommon("data-provided-by")}{" "} - - coingecko.com - -
- } - > - - -
-
-
- - {t("page-index-what-is-ether-action")} - -
- - - - {/* Apps of the week - Discover the best apps on Ethereum */} - {/* // TODO: Remove locale restriction after translation */} - {locale === DEFAULT_LOCALE && ( -
- -
- Apps of the week - Discover apps on Ethereum -

Start exploring Ethereum today

-
- ({ - ...app, - description: getLocalizedDescription( - appDescriptions, - "app", - app.name, - app.description - ), - }))} - matomoCategory="apps-of-the-week" - /> -
- - Browse apps - -
-
-
- )} - - {/* Activity - The strongest ecosystem */} -
- - - - - - {t("page-index-activity-tag")} - {t("page-index-activity-header")} -
-

- {t("page-index-activity-description")} -

-

- {t("page-index-activity-subtitle")} -

- - -
- - {t("page-index-activity-action-primary")} - - - {t("page-index-activity-action")} - -
-
-
-
- - {/* Values - The Internet Is Changing */} -
- - {t("page-index-values-tag")} - {t("page-index-values-header")} -

- {t("page-index-values-description")} -

-
- - {/* dynamic / lazy loaded */} - - - -
- - {/* Builders - Blockchain's biggest builder community */} -
- - - - - - {t("page-index-builders-tag")} - {t("page-index-builders-header")} -

{t("page-index-builders-description")}

-
- - {t("page-index-builders-action-primary")} - - - {t("page-index-builders-action-secondary")} - -
-
- {/* CLIENT SIDE */} - -
-
-
- - {/* Recent posts */} -
-

- {t("page-index-posts-header")} -

-

{t("page-index-posts-subtitle")}

- - {/* dynamic / lazy loaded */} - - -
-

{t("page-index-posts-action")}

-
- {blogLinks.map(({ name, href }) => ( - - {name} - - ))} -
-
-
- - {/* Events */} -
-

- {t("page-index-events-header")} -

-

{t("page-index-events-subtitle")}

-
-
- {upcomingEvents.map( - ( - { - id, - title, - link, - location, - startTime, - endTime, - bannerImage, - }, - idx - ) => ( - - - {bannerImage ? ( - - ) : ( - - )} - - - {title} - - {formatDateRange(startTime, endTime, locale, { - month: "long", - year: "numeric", - })} - - - {location} - - - - ) - )} -
-
-
- - {t("page-index-events-action")} - -
-
- - {/* Join ethereum.org */} -
-
-
-

{t("page-index-join-header")}

-

{t("page-index-join-description")}

-
-
- {joinActions.map( - ({ Svg, label, href, className, description, eventName }) => ( - -

{description}

-
- ) - )} -
-
- - {t("page-index-join-action-hub")} - -
-
-
- -
+ ) } diff --git a/src/components/Hero/HomeHero2026/index.tsx b/src/components/Hero/HomeHero2026/index.tsx index d5f8c5b12d9..cbfc49e9d31 100644 --- a/src/components/Hero/HomeHero2026/index.tsx +++ b/src/components/Hero/HomeHero2026/index.tsx @@ -125,7 +125,7 @@ const HomeHero2026 = ({

- The internet that the world can rely on. + The internet that belongs to you

diff --git a/src/components/Homepage/FeatureCards.tsx b/src/components/Homepage/FeatureCards.tsx index a26e9e76829..14b1d59f575 100644 --- a/src/components/Homepage/FeatureCards.tsx +++ b/src/components/Homepage/FeatureCards.tsx @@ -32,7 +32,7 @@ const FeatureCards = ({ What makes Ethereum different

- Principles set Ethereum apart from traditional systems + Principles that set Ethereum apart from traditional systems

diff --git a/src/components/Homepage/Homepage2026.tsx b/src/components/Homepage/Homepage2026.tsx index 59a06d9c6e5..f0683f2e86b 100644 --- a/src/components/Homepage/Homepage2026.tsx +++ b/src/components/Homepage/Homepage2026.tsx @@ -71,10 +71,6 @@ const Homepage2026 = ({ - - - - }> @@ -83,6 +79,10 @@ const Homepage2026 = ({ + + + + diff --git a/src/components/Homepage/PersonaModalCTA.tsx b/src/components/Homepage/PersonaModalCTA.tsx index 149661506b7..69d7723eedf 100644 --- a/src/components/Homepage/PersonaModalCTA.tsx +++ b/src/components/Homepage/PersonaModalCTA.tsx @@ -1,7 +1,13 @@ "use client" import { useRef, useState } from "react" -import { BookOpen, Building2, Code, ExternalLink } from "lucide-react" +import { + AppWindowMac, + BookOpen, + Building2, + Code, + ExternalLink, +} from "lucide-react" import { ChevronNext } from "@/components/Chevron" import { Button } from "@/components/ui/buttons/Button" @@ -50,45 +56,117 @@ const categories: PersonaCategory[] = [ eventName: "learn_ethereum", }, { - label: "Get a wallet", + label: "Pick a wallet", href: "/wallets/find-wallet/", - eventName: "get_wallet", + eventName: "pick_wallet", }, ], }, { - id: "developers", - label: "For developers", - Icon: Code, - iconBgClass: "bg-primary-low-contrast", - iconColorClass: "text-primary", + id: "explorers", + label: "For explorers", + Icon: AppWindowMac, + iconBgClass: "bg-accent-c/20", + iconColorClass: "text-accent-c", links: [ { - label: "Developer Hub", - href: "/developers/", - eventName: "developer_hub", + label: "Get ETH", + href: "/get-eth/", + eventName: "get_eth", }, - { label: "Docs", href: "/developers/docs/", eventName: "docs" }, + { label: "Try apps", href: "/apps/", eventName: "try_apps" }, ], }, { - id: "enterprise", - label: "For enterprise", - Icon: Building2, - iconBgClass: "bg-accent-c/20", - iconColorClass: "text-accent-c", + id: "builders", + label: "For builders", + Icon: Code, + iconBgClass: "bg-primary-low-contrast", + iconColorClass: "text-primary", links: [ - { label: "Founders", href: "/founders/", eventName: "founders" }, { - label: "Institutions", - href: ENTERPRISE_ETHEREUM_URL, - isExternal: true, - eventName: "institutions", + label: "Start building", + href: "/developers/", + eventName: "start_building", }, + { label: "Docs", href: "/developers/docs/", eventName: "docs" }, ], }, ] +const enterpriseCategory: PersonaCategory = { + id: "enterprise", + label: "For enterprise", + Icon: Building2, + iconBgClass: "bg-accent-c/20", + iconColorClass: "text-accent-c", + links: [ + { label: "Founders", href: "/founders/", eventName: "founders" }, + { + label: "Institutions", + href: ENTERPRISE_ETHEREUM_URL, + isExternal: true, + eventName: "institutions", + }, + ], +} + +const CategoryCard = ({ + category: { id, label, Icon, iconBgClass, iconColorClass, links }, + onLinkClick, + className, +}: { + category: PersonaCategory + onLinkClick: (eventName: string) => void + className?: string +}) => ( +
+
+
+ +
+

{label}

+
+ +
+ {links.map(({ label: linkLabel, href, isExternal, eventName }, idx) => ( +
+ {idx > 0 &&
} + onLinkClick(eventName)} + hideArrow + className="group flex items-center justify-between text-xl font-bold text-primary no-underline transition-colors hover:text-primary-hover md:text-3xl" + {...(isExternal && { + target: "_blank", + rel: "noopener noreferrer", + })} + > + + {linkLabel} + {isExternal && ( + + )} + + + +
+ ))} +
+
+) + type PersonaModalCTAProps = { eventCategory: string } @@ -152,61 +230,19 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => {
- {categories.map( - ({ id, label, Icon, iconBgClass, iconColorClass, links }) => ( -
- {/* Icon and Category Label */} -
-
- -
-

- {label} -

-
- - {/* Links */} -
- {links.map( - ( - { label: linkLabel, href, isExternal, eventName }, - idx - ) => ( -
- {idx > 0 &&
} - handleLinkClick(eventName)} - hideArrow - className="group flex items-center justify-between text-xl font-bold text-primary no-underline transition-colors hover:text-primary-hover md:text-3xl" - {...(isExternal && { - target: "_blank", - rel: "noopener noreferrer", - })} - > - - {linkLabel} - {isExternal && ( - - )} - - - -
- ) - )} -
-
- ) - )} + {categories.map((category) => ( + + ))} + {/* Enterprise card: mobile only */} +
diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index 1e0527f96f4..a1e4d7019fe 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -27,18 +27,6 @@ import borrowingImage from "@/public/images/homepage/savings/borrowing.png" import defiImage from "@/public/images/homepage/savings/defi.png" import remittancesImage from "@/public/images/homepage/savings/remittances.png" -const APY_DATA = { - traditional: { - label: "Traditional Savings", - apy: 0.5, - }, - ethereum: { - label: "Ethereum Apps", - apyMin: 4, - apyMax: 8, - }, -} as const - type ComparisonItem = { label: string value: string @@ -60,21 +48,33 @@ type Slide = { cta: string href: string image: typeof defiImage - comparison: ComparisonData | "apy" + comparison: ComparisonData } const slides: Slide[] = [ { - id: "defi", - tag: "SAVINGS & INTEREST", - title: "Ownership has financial benefits too", - subtitle: "When there's no broker taking a cut, you gain more.", + id: "privacy", + tag: "YOUR BUSINESS IS YOURS", + title: "Use the internet without being watched", + subtitle: + "Most apps track what you do, who you talk to, and what you own. They sell that data or hand it over when asked. On Ethereum, your activity can stay private.", description: - "Earn higher interest on funds using lending apps on Ethereum. You can withdraw your money 24/7.", - cta: "See DeFi →", - href: "/defi/", + "No account tied to your name. No company watching your balance.", + cta: "Use privacy preserving apps →", + href: "/apps/categories/privacy/", image: defiImage, - comparison: "apy", + comparison: { + traditional: { + label: "TRADITIONAL APPS", + value: "Your data is their product", + smallText: true, + }, + ethereum: { + label: "ETHEREUM APPS", + value: "Private by default", + smallText: true, + }, + }, }, { id: "remittances", @@ -82,8 +82,8 @@ const slides: Slide[] = [ title: "Send money home in 12 minutes", subtitle: "Skip the $50 wire fee and the 5+ day wait.", description: - "Send stablecoins for just $0.2, and your family receives the funds almost instantly.", - cta: "Send money →", + "Send stablecoins to anyone, anywhere in the world, for just $0.02. They receive the funds almost instantly.", + cta: "Try it yourself →", href: "/payments/", image: remittancesImage, comparison: { @@ -98,8 +98,8 @@ const slides: Slide[] = [ subtitle: "You don't need a credit score to get started.", description: "Using DeFi apps on Ethereum, you can provide collateral and access credit instantly, no permission required.", - cta: "Try it yourself →", - href: "/apps/categories/defi/", + cta: "Learn more about DeFi →", + href: "/defi/", image: borrowingImage, comparison: { traditional: { @@ -116,24 +116,6 @@ const slides: Slide[] = [ }, ] -const getComparison = (slide: Slide): ComparisonData => { - if (slide.comparison === "apy") { - return { - traditional: { - label: APY_DATA.traditional.label, - value: `${APY_DATA.traditional.apy}%`, - suffix: "APY", - }, - ethereum: { - label: APY_DATA.ethereum.label, - value: `${APY_DATA.ethereum.apyMin}-${APY_DATA.ethereum.apyMax}%`, - suffix: "APY", - }, - } - } - return slide.comparison -} - type SavingsCarouselProps = { className?: string eventCategory?: string @@ -203,7 +185,7 @@ const SlideContent = ({ isActive, eventCategory, }: SlideContentProps) => { - const comparison = getComparison(slide) + const comparison = slide.comparison const traditionalControls = useAnimationControls() const ethereumControls = useAnimationControls() diff --git a/src/components/Homepage/TrustLogos.tsx b/src/components/Homepage/TrustLogos.tsx index 27ee4db37dc..0da84e0c26f 100644 --- a/src/components/Homepage/TrustLogos.tsx +++ b/src/components/Homepage/TrustLogos.tsx @@ -1,5 +1,4 @@ import { ArrowRight, Check } from "lucide-react" -import type { StaticImageData } from "next/image" import { Image } from "@/components/Image" import { BaseLink } from "@/components/ui/Link" @@ -12,32 +11,9 @@ import { import { cn } from "@/lib/utils/cn" -import { ENTERPRISE_ETHEREUM_URL } from "@/lib/constants" - import FloatingCard from "./FloatingCard" import builtToLastImage from "@/public/images/homepage/built-to-last.png" -import blackrockLogo from "@/public/images/homepage/logos/blackrock.webp" -import jpmorganLogo from "@/public/images/homepage/logos/jpmorgan.png" -import mastercardLogo from "@/public/images/homepage/logos/mastercard.png" -import paypalLogo from "@/public/images/homepage/logos/paypal.png" -import robinhoodLogo from "@/public/images/homepage/logos/robinhood.png" -import visaLogo from "@/public/images/homepage/logos/visa.png" - -type Logo = { - src: StaticImageData - alt: string - className?: string -} - -const logos: Logo[] = [ - { src: mastercardLogo, alt: "Mastercard", className: "h-10" }, - { src: visaLogo, alt: "Visa" }, - { src: jpmorganLogo, alt: "JPMorgan" }, - { src: robinhoodLogo, alt: "Robinhood" }, - { src: paypalLogo, alt: "PayPal" }, - { src: blackrockLogo, alt: "BlackRock" }, -] type TrustLogosProps = { className?: string @@ -91,48 +67,35 @@ const TrustLogos = ({
- - Trusted by leading institutions - + Proven track record Built to last
-

- Major financial institutions choose Ethereum because it's the - most battle-tested, low-risk, and dependable blockchain. The code is - open, the network is always on, and the track record speaks for - itself. -

+
+

+ Ethereum has run continuously since 2015 without a single second of + downtime. +

+

+ The code is open for anyone to verify. No company runs it, no one + can shut it down, and thousands of independent operators keep it + going worldwide. +

+
- See institutional adoption + Get ETH - -
- {logos.map((logo) => ( -
- {logo.alt} -
- ))} -
) From 49fdf3123da8f107abd7b903ef216541e632a49e Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 14 Apr 2026 16:55:09 +0200 Subject: [PATCH 002/109] fix: match floating card font sizes to figma (14px label, 24px value) --- src/components/Homepage/SavingsCarousel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index a1e4d7019fe..7e269cb7cbe 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -150,7 +150,7 @@ const ComparisonCard = ({

@@ -160,7 +160,7 @@ const ComparisonCard = ({ {item.value} From 2036d791416a86f6ecdf97c0ce14ef0e9eb2afb1 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 15 Apr 2026 19:11:25 +0200 Subject: [PATCH 003/109] chore: remove unused enterprise logo images --- public/images/homepage/logos/blackrock.webp | Bin 1174 -> 0 bytes public/images/homepage/logos/jpmorgan.png | Bin 3139 -> 0 bytes public/images/homepage/logos/mastercard.png | Bin 1915 -> 0 bytes public/images/homepage/logos/paypal.png | Bin 4987 -> 0 bytes public/images/homepage/logos/robinhood.png | Bin 4972 -> 0 bytes public/images/homepage/logos/visa.png | Bin 3242 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/images/homepage/logos/blackrock.webp delete mode 100644 public/images/homepage/logos/jpmorgan.png delete mode 100644 public/images/homepage/logos/mastercard.png delete mode 100644 public/images/homepage/logos/paypal.png delete mode 100644 public/images/homepage/logos/robinhood.png delete mode 100644 public/images/homepage/logos/visa.png diff --git a/public/images/homepage/logos/blackrock.webp b/public/images/homepage/logos/blackrock.webp deleted file mode 100644 index 33996606e157a78c6f8241fed802206435c226cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1174 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 Bes%x= diff --git a/public/images/homepage/logos/paypal.png b/public/images/homepage/logos/paypal.png deleted file mode 100644 index 187d5ae668083d9c3839f9bbd78900ecab7d1fd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4987 zcmV->6NK!EP)Px|B}qgV1Qj~LKVGQq3> zvw{vQAglm`MHMs5ELCy3%?dCp09FuZ1-K`m?jE#y1+STOyDPTNJMdLXaco&S_sWuk zepOUgC%PZ;y*@hkd|jZXrlzK*rlzK*rlzJw2xWcH^Ssvp+C@!-nI{1LvMeiP=5Y`N zqoT&DsV<#Pr)k@EkBIIUH3l<>0KO4X2w>dr_eVvIbrX@U5i`GavOMT{-VuPFU~ZWq zTF5?Tj>iOm5rFs8>GWe5hLa-4uc-!VBR?y0jCilb_`W|OqS1Ul|Mc_EKM#u>`z9b= zBO+>9Ss%m<;$0a3cOuM zx<*74W)e8eT*f9y;sQ7VaHfuSH8+=LnS&FN%glprxBKUd7cY*=oJ)lftr0VS%c|j& zx(O1v0D60Sd!K7<=f+Z&Hhl#ma+SvOuG8sMW&c+N(Hb%HIID(JiA@lrj(BJH?%k`@ zylV95bUKw=Pa-<9ZToYl({an5OBK*rqfAGt3V*3x%d%>rbAzyL+bw$zs~{rRw(YZ8 z=xDFeW(~)29Gw9Qg@~G#Wu4W>tD8ZI&WVlGLT7=_8qH?2%>t)t$EjtMed)|~-TIup z<3v$Z^}Xggj`ObUd6bF98cha+!DQzJj>5fj=T7at+How)`d`^|$Uxio{Z84FC<~1> z`V{~+3Y;C;1YqWl=Xve2=TW1@F1*|T;PrRxxpF0qn0X9j5I8%c;l$Kh&W=(=4^;|H z$8o+pEk~`9NYJV@l5~NygC3xEt>x?{j^pf_#_;TW({eP|Xbb?Uxm31o>kRN$#^;P4 z*_indiyW+)-9qQ(R>odyI*xM$U|_J%s-qKwGXXXT93pDP$&`7S&*z7K{PD*)%d-au z2Q6j&ehuLnF~Ji6-<`U<>oBvsoK%^We!uT#efH(cmn~*KB%&wGoVnc^e!_PoaCFw_ z$z(mAb&RHi6s%O=iCQJcwu# zMNtSKB%)6MMnMpSy5oM2IF2L!(pr|KwburN!KB;mzLysLvMfvMNY&|dn(FU=s9x|b zwa=3MP5_LVIb`PZD2hgd!64KbH!r5vXsy7RPNzB+X>EZso6SbHZR>WE3(xb~su0r? zW^N`8=F7%&W^TqC1i&Ex?CtH1iRf(*1j7}5yUg4nqW@%>3p0NsqQSBdQ`HwA=KITV zzG_Z792^|9=JWYc*1soa)!(IR(1_-t%7toSS=LxOK7?<5-}l>CI7u#JTT7by0 zcxYMHF9zJB%eYtwo+kjF20;*-h}a&F$CLg2{Ymm@mlgHKGa_&R^dxhLS=NB1?p7ii z4+ev>Yh7m=U*%uDhT)2aMC20DyKcApa6X?u2jJ!vHmaZ0d6`;cZz~xcoC_{)9!WQC=RpuG`ctM!&zmf2cGsDZ}OC zoitxB>x(4=IxmCN{od(x;tcb)#uyt>g~HlIBy#m{^7ffpqqSZtX<1Q&o1iUim?rcN z4h~w4M&t7qgiVag%pW8T*LeGelD>$j*=RJhwJc`-MYD@lArIn`m6~$_07OyLmbQ%} zXjjqgb`KrL`HU@vX(-J`x;~peOCxKvR^UjF0H!tpWN&c*z;rrYIS0@4o-p$ncEoMI zN^mwBjd9v0PeWJ8DqEMU(Z!o(Be-tKN{zY1ewE(%_@O29*-yLSS3L$#Fe2~=lYaT z1$ACQw`8S8TL6ILI8PuK3g@eS===U_6nu}HY0?bmCf8`Cz|koys%yhhX$j&o6Hydh z+XM#(2Q3JXB4v`|?Hft@Q)wS<;dFY?)NWS&P`6~Id^!vFF*F1N5nVIFE90=Z&$3vj zsR3r!Xr;i>@u;jj&SXIe8?7Tx6h*`3KBFiS*-*-6owu*iXnd3&t01r^sC6-FD2~xB zSt*^6idE6t7q)GWlK-#j9qZb-w$3Ic!Q>jP^isv~D0yM#iNp)yx^A=4XgnjLj;_0P z`Af>oxnmNlK-wl4kH-_w^Tv=FBPI5VGeMgqYjfi)%gPlvnBZ4Qqc|3ZnS0XM9{?bt z;qmctnCREFZCm$qDuqps9+PXdQsCr`9!<2%ya|-%(ITSus>ogm%h|sw0B*;P&m?!|8M?Qr~aizI}uY zbzF4qX;@<@j?qjA{K@zI%o~T9p`+0xA);Z@=Amlr=o~pqCC%HXxkjVqA6&EHC~w5h zcybaftL;5Q{r<(wZ;9wJfV+u*kBI0w)_5um5T^ZdJRVOVHy;0#@zEX%Fcin=Bm|a; zZQED=E`v70a{zCc`SENvyBq(*BITgSy|(t{rkS)EUh8kgbFOrN5*e=0Zgk%`=Ky*^ z5RBIRKE&b+KS*d(#@=#SynWJR6*IRs9jl5o6erk4f%7J5yLs{A#gT!d)H#4&zuzCN z`F#kr*L>e^6VW@9={Os_ed0A*221FFvwtXd=TDig7(*B6(Q66EFq~GuN43tO(HhB9?mwvq_aHL6_Wx&kO zzkKP`t!09jP8Ck5a(GGo{mCTCiOf z=JR<^H@$kbkeQ$A^pW;*E28x46@k-gwVJ!&p;DSi zZ_TgUf-XZCI10t6;KPwd7h%>1)zL*dRy+AW20{t47>WVxjE8ChVCAu=)9JK?b!WnU zzu(iInL^ogbV`mC<)PXvaCFLwZix`Uqjj2yMxK!Idc+z>9Z5c#)Lpv#hVBXbiODRq ztNvmmqR?@i2kSgXmSsu5E5=@IG0ZH!c_J-EMrVzpC|cR?6@eqIfwK*W=;L%ceURd& z79R)a^SKG*+cKfF;e0;N`^FjyhiWUEO&n3IV{Ff4D2}P}(Vdy!UR+!}Shm?Dk({TN49~iP{=*_lqB65s&CNTXAP7Rw^ZtV1 zwu(g5@wN=b02T>>ain>LI0w)>Jv|+5dQj-SIWM9lDiKX2Aq-iPHic`n*42JZ;8Z9s zVpHJVHH02Etvj?#qfy>t2!YTbqMtm^`w#Ro^m7}@r6ns1VpHJVD(>bT#9(ncBNSCu z6e+IuD+0%|tpAl9^mmSh0dy#_si?c7@;2M#78D_zi_Rs2BM1ZzGq)_u(h2q){{H*# zl4C-d+VljAFU7~%Y&H~X9l5`ZCMp!JLL=(9kfhBt!D{SC#mt{_)@TXi|5}zc{`>F0 zL)`-3%>3WYPi>91q>)s^34&lGxzRx?7**Q|Gv_vf>XfVmkh|ic`l3(N{v4ZdNmmK! z(Pvv_MUz|#9G$YF>S8mk*pnNGC`LoGV&Z!MI%Pm}!OU+Zny606N+NpR@Arq&_GMDQ zbwmIFX-(#Jo+O~S(yy0dQKzh^MvdJn&^F!Sy?ggsQf*P^5{2)7?Q1j|vp@Z zv4|AE5{6@N)Q*3Z)VclHy)2C zdwYB5Ic{gzLaR&f)kWY1B_ z)5wi!Z{MnWuchc3ikHZp^r+@gsU%wfn9XKGRLgUgl}&RNX~tRBDE3l`Hi1T>VbK^b za#S>?y?vVZk0%LDm0nfe4^akkmFzb$>R7UKdkrZDW3@(7k9D}|8LQQ5i8nzNH5}m5 zcw9tLts)tj>})(9PfWPk4H=37=y^YE+a8&khe`VV{s23sQE%`hxzc{KtdVt7JXFiv zzJ0p~LvH46u#fTck@7-acm5F&c+3d|W7^WMvgNK0Xe0Z%^1J+b%1b#Jb5zn?VqSM6?q^$F#Rk z<#e^B?F}1>0o?4Q&C}D{?p_8SosIcoyYa|vpRZ7}SyoTP}7l~v;8{RT*VPm-?3tP^F~JO{h2ZBNS`g3#EqY0=#`a=tVw1 z+Wd$J>C3Isnsk!W)6g1b_imPrlAz}@PideWPC_3D)?ZCmh{*Eq+=$KlDz$z39PYI5%CKJusie!tDk zqUh5!57jo-$b#^#&&;BBWfKI!FbIOXSO^|z#_y$|`RAX1?j{l%N5X!F(s`^vCyO{l zL2)J1>2xCe@=YAaDYWE9QJkEd3@%(=fTU9XpS7sg)(Z_Yy*C-9c%{sox&vJi%zr)PG zXms&3>s7rHUcGwd&So<=Pn+3nHms^%fcsb<(L2GyCNcsrT6cYo@B8gM z<7fFSi(aG3TtZb9T-R;>@WT&o6h%z{uqk=x>uVzbhI-TaN zTesYJ&4_4nad9yj3A&l^I|c5}DtLbYc1soY(wHTf}cFL)t_;+mRe zYJqbz(2BXiZYChs>>#zk*>OD2dkwvqn>V#~Q?o5<8_o`+JYqU`qt}Mwnypf6I6Djx zebDTpsEFd4nw8W7XGifo??@-{7PXPx|7D+@wRCt{2o#A!dHnWD`d?)|2w}K!QSg9aP1=pz{ zbOli=5L1Db3Pe|6r2^3v*r{OQ3bbDZx<9bE%caSg0Wcs}(mdzz*jjQ1LlD3K-~jv% zhG7_nk;e{z18{XbemMReIRN+yzytUKU|7>Q!!Qiv<*@_M0JsJ48NgHGu?KK-)MFTi zVHjc>01p5jk?wq!9Fkkne#0;f;~8@U9p^JW9@e}G!!Qivc~Z*>#iLp6P7K2^jAut} zCuE}{!!QhEku{u9JS=d*Fbu;uV@)UY1mNxW>)h9?UdA zEUCjCmLqZgL|DgQ7#uBt-xtn(K`2Wvl^X!v%4s&{i1di@^_PqTZR4kP%&5k%0DfFJ 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 From 3d2de6af5d70ca0121c765413237db7686818c79 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 15 Apr 2026 19:20:27 +0200 Subject: [PATCH 004/109] fix: throw on missing data instead of silently catching --- app/[locale]/page.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index d5f4a42eb4c..5f5c44b87ba 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -22,19 +22,22 @@ const Page = async (props: { params: Promise }) => { setRequestLocale(locale) const [accountHoldersData, growThePieData] = await Promise.all([ - getAccountHolders().catch(() => null), - getGrowThePieData().catch(() => null), + getAccountHolders(), + getGrowThePieData(), ]) + if (!accountHoldersData) { + throw new Error("Failed to fetch account holders data") + } + if (!growThePieData) { + throw new Error("Failed to fetch GrowThePie data") + } + const accountHolders = - accountHoldersData && "value" in accountHoldersData - ? accountHoldersData.value - : null + "value" in accountHoldersData ? accountHoldersData.value : null const transactionsToday = - growThePieData?.txCount && "value" in growThePieData.txCount - ? growThePieData.txCount.value - : null + "value" in growThePieData.txCount ? growThePieData.txCount.value : null return ( <> From 1d3ef9845c2102b3c108ab25a299ec480b4ea048 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 15 Apr 2026 19:42:05 +0200 Subject: [PATCH 005/109] feat: internationalize homepage 2026 components and clean up old keys --- src/components/Hero/HomeHero2026/index.tsx | 81 +++--- src/components/Homepage/FeatureCards.tsx | 60 +++-- src/components/Homepage/GetStartedGrid.tsx | 121 +++++---- src/components/Homepage/KPISection.tsx | 23 +- src/components/Homepage/PersonaModalCTA.tsx | 145 +++++----- src/components/Homepage/SavingsCarousel.tsx | 125 ++++----- .../Homepage/SimulatorSection/index.tsx | 8 +- src/components/Homepage/TrustLogos.tsx | 33 ++- src/intl/en/page-index.json | 250 ++++++++---------- 9 files changed, 433 insertions(+), 413 deletions(-) diff --git a/src/components/Hero/HomeHero2026/index.tsx b/src/components/Hero/HomeHero2026/index.tsx index cbfc49e9d31..ba99ce10030 100644 --- a/src/components/Hero/HomeHero2026/index.tsx +++ b/src/components/Hero/HomeHero2026/index.tsx @@ -1,5 +1,6 @@ import { Fragment } from "react" import { getImageProps, type StaticImageData } from "next/image" +import { getTranslations } from "next-intl/server" import type { ClassNameProp } from "@/lib/types" @@ -29,42 +30,7 @@ type HomeHero2026Props = ClassNameProp & { eventCategory?: string } -const directButtonCTAs = [ - { - label: "Learn Ethereum", - description: "What is Ethereum?", - href: "/what-is-ethereum/", - Svg: EthGlyphIcon, - className: "text-accent-a hover:text-accent-a-hover", - eventName: "learn_ethereum", - }, - { - label: "Pick a wallet", - description: "Create accounts, manage assets", - href: "/wallets/find-wallet/", - Svg: EthWalletIcon, - className: "text-primary hover:text-primary-hover", - eventName: "pick_wallet", - }, - { - label: "Get ETH", - description: "The currency of Ethereum", - href: "/get-eth/", - Svg: EthTokenIcon, - className: "text-accent-b hover:text-accent-b-hover", - eventName: "get_eth", - }, - { - label: "Try apps", - description: "See what Ethereum can do", - href: "/dapps/", - Svg: TryAppsIcon, - className: "text-accent-c hover:text-accent-c-hover", - eventName: "try_apps", - }, -] - -const HomeHero2026 = ({ +const HomeHero2026 = async ({ className, image, image2xl, @@ -72,9 +38,45 @@ const HomeHero2026 = ({ ctaVariant = "modal", eventCategory = "Homepage", }: HomeHero2026Props) => { + const t = await getTranslations("page-index") const baseImage = image ?? heroBase const xlImage = image2xl ?? image ?? hero2xl - const alt = altProp ?? "Ethereum illustration" + const alt = altProp ?? t("page-index-hero-image-alt") + + const directButtonCTAs = [ + { + label: t("page-index-cta-learn-label"), + description: t("page-index-modal-what-is-ethereum"), + href: "/what-is-ethereum/", + Svg: EthGlyphIcon, + className: "text-accent-a hover:text-accent-a-hover", + eventName: "learn_ethereum", + }, + { + label: t("page-index-cta-wallet-label"), + description: t("page-index-cta-wallet-description"), + href: "/wallets/find-wallet/", + Svg: EthWalletIcon, + className: "text-primary hover:text-primary-hover", + eventName: "pick_wallet", + }, + { + label: t("page-index-cta-get-eth-label"), + description: t("page-index-cta-get-eth-description"), + href: "/get-eth/", + Svg: EthTokenIcon, + className: "text-accent-b hover:text-accent-b-hover", + eventName: "get_eth", + }, + { + label: t("page-index-cta-dapps-label"), + description: t("page-index-cta-dapps-description"), + href: "/dapps/", + Svg: TryAppsIcon, + className: "text-accent-c hover:text-accent-c-hover", + eventName: "try_apps", + }, + ] const common = { alt, @@ -125,12 +127,11 @@ const HomeHero2026 = ({

- The internet that belongs to you + {t("page-index-hero-title")}

- Ethereum is the global network where you control your assets, your - data, and your identity. + {t("page-index-hero-subtitle")}

{ctaVariant === "modal" ? ( diff --git a/src/components/Homepage/FeatureCards.tsx b/src/components/Homepage/FeatureCards.tsx index 14b1d59f575..19a0ed70675 100644 --- a/src/components/Homepage/FeatureCards.tsx +++ b/src/components/Homepage/FeatureCards.tsx @@ -1,3 +1,5 @@ +import { getTranslations } from "next-intl/server" + import { ChevronNext } from "@/components/Chevron" import { Image } from "@/components/Image" import { ButtonLink } from "@/components/ui/buttons/Button" @@ -15,10 +17,12 @@ type FeatureCardsProps = { eventCategory?: string } -const FeatureCards = ({ +const FeatureCards = async ({ className, eventCategory = "Homepage", }: FeatureCardsProps) => { + const t = await getTranslations("page-index") + return (
- What makes Ethereum different + {t("page-index-features-title")}{" "} + + {t("page-index-features-title-highlight")} +

- Principles that set Ethereum apart from traditional systems + {t("page-index-features-subtitle")}

@@ -53,20 +60,27 @@ const FeatureCards = ({

- Direct ownership + {t("page-index-features-ownership-title")}

- Your bank balance is a{" "} - custody promise.{" "} + {t("page-index-features-ownership-description-1")}{" "} + + {t("page-index-features-ownership-description-custody")} + + .{" "} - Your Ethereum balance is true ownership. + {t("page-index-features-ownership-description-2")}

-

4.6B+

-

Daily trading volume

+

+ {t("page-index-features-ownership-stat")} +

+

+ {t("page-index-features-ownership-stat-label")} +

@@ -81,12 +95,10 @@ const FeatureCards = ({

- Public rules + {t("page-index-features-public-rules-title")}

- The code is public, agreements execute exactly as written. - Think vending machine versus hoping the cashier gives correct - change. + {t("page-index-features-public-rules-description")}

@@ -102,9 +114,11 @@ const FeatureCards = ({ />
-

Global

+

+ {t("page-index-features-global-title")} +

- Anyone, anywhere can use Ethereum. No permission needed. + {t("page-index-features-global-description")}

@@ -118,19 +132,21 @@ const FeatureCards = ({ />
-

Free access

+

+ {t("page-index-features-free-access-title")} +

- No credit check, no minimum balance, no account approval. If - you have internet, you're in. + {t("page-index-features-free-access-description")}

-

Nobody owns Ethereum

+

+ {t("page-index-features-nobody-owns-title")} +

- Changes happen through open proposals that anyone can - participate in. Think community garden versus corporate farm. + {t("page-index-features-nobody-owns-description")}

@@ -146,7 +162,7 @@ const FeatureCards = ({ eventName: "feature_cards/learn_more", }} > - What is Ethereum? + {t("page-index-features-cta")} diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx index 1d8cb940d65..0d2b6cbc49c 100644 --- a/src/components/Homepage/GetStartedGrid.tsx +++ b/src/components/Homepage/GetStartedGrid.tsx @@ -1,4 +1,5 @@ import { Book, Building2, ChevronRight, Code } from "lucide-react" +import { getTranslations } from "next-intl/server" import { Image } from "@/components/Image" import { Card, CardContent } from "@/components/ui/card" @@ -13,82 +14,80 @@ import developersImage from "@/public/images/homepage/get-started/developers.png import enterpriseImage from "@/public/images/homepage/get-started/enterprise.png" import learnImage from "@/public/images/homepage/get-started/learn.png" -const cards = [ - { - id: "learn", - icon: Book, - iconBg: "bg-[#f7ecff]", - iconColor: "text-primary", - title: "Understand Ethereum", - description: - "Start here. Learn what it is, why it matters, and how it works in plain language.", - bullets: [ - "What is Ethereum?", - "How do wallets work?", - "DeFi, stablecoins, and NFTs explained", - ], - bulletColor: "bg-primary", - cta: "Start learning", - href: "/learn/", - image: learnImage, - }, - { - id: "developers", - icon: Code, - iconBg: "bg-[#e9f4ff]", - iconColor: "text-accent-a", - title: "Start building", - description: - "For developers. Access documentation, tools, and tutorials to build on Ethereum.", - bullets: [ - "Developer documentation", - "Smart contract tutorials", - "Development tools & frameworks", - ], - bulletColor: "bg-accent-a", - cta: "View materials", - href: "/developers/", - image: developersImage, - }, - { - id: "enterprise", - icon: Building2, - iconBg: "bg-[#e6f7f6]", - iconColor: "text-accent-c", - title: "For enterprise", - description: - "Business use cases, institutional resources, and how Ethereum can serve your organization.", - bullets: [ - "Enterprise use cases", - "Private & permissioned networks", - "Institutional resources", - ], - bulletColor: "bg-accent-c", - cta: "Explore enterprise", - href: ENTERPRISE_ETHEREUM_URL, - image: enterpriseImage, - }, -] - type GetStartedGridProps = { className?: string eventCategory?: string } -const GetStartedGrid = ({ +const GetStartedGrid = async ({ className, eventCategory = "Homepage", }: GetStartedGridProps) => { + const t = await getTranslations("page-index") + + const cards = [ + { + id: "learn", + icon: Book, + iconBg: "bg-[#f7ecff]", + iconColor: "text-primary", + title: t("page-index-get-started-learn-title"), + description: t("page-index-get-started-learn-description"), + bullets: [ + t("page-index-get-started-learn-bullet-1"), + t("page-index-get-started-learn-bullet-2"), + t("page-index-get-started-learn-bullet-3"), + ], + bulletColor: "bg-primary", + cta: t("page-index-get-started-learn-cta"), + href: "/learn/", + image: learnImage, + }, + { + id: "developers", + icon: Code, + iconBg: "bg-[#e9f4ff]", + iconColor: "text-accent-a", + title: t("page-index-get-started-build-title"), + description: t("page-index-get-started-build-description"), + bullets: [ + t("page-index-get-started-build-bullet-1"), + t("page-index-get-started-build-bullet-2"), + t("page-index-get-started-build-bullet-3"), + ], + bulletColor: "bg-accent-a", + cta: t("page-index-get-started-build-cta"), + href: "/developers/", + image: developersImage, + }, + { + id: "enterprise", + icon: Building2, + iconBg: "bg-[#e6f7f6]", + iconColor: "text-accent-c", + title: t("page-index-get-started-enterprise-title"), + description: t("page-index-get-started-enterprise-description"), + bullets: [ + t("page-index-get-started-enterprise-bullet-1"), + t("page-index-get-started-enterprise-bullet-2"), + t("page-index-get-started-enterprise-bullet-3"), + ], + bulletColor: "bg-accent-c", + cta: t("page-index-get-started-enterprise-cta"), + href: ENTERPRISE_ETHEREUM_URL, + image: enterpriseImage, + }, + ] + return (
- Get started on Ethereum + {t("page-index-get-started-title")}

- Takes 2 minutes to get started. No credit check, no paperwork, no - minimum balance. + {t("page-index-get-started-subtitle")}

diff --git a/src/components/Homepage/KPISection.tsx b/src/components/Homepage/KPISection.tsx index 7612c815475..5e9f02d6cec 100644 --- a/src/components/Homepage/KPISection.tsx +++ b/src/components/Homepage/KPISection.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react" import { ArrowLeftRight, User } from "lucide-react" +import { useLocale, useTranslations } from "next-intl" import { useIntersectionObserver } from "usehooks-ts" import { Section, SectionHeader, SectionTag } from "@/components/ui/section" @@ -126,7 +127,7 @@ function AnimatedNumber({ /** * Format large numbers with M/B suffix */ -function formatNumber(value: number): string { +function formatNumber(value: number, locale: string): string { if (value >= 1_000_000_000) { return `${(value / 1_000_000_000).toFixed(1)}B` } @@ -134,7 +135,7 @@ function formatNumber(value: number): string { return `${Math.round(value / 1_000_000)}M` } if (value >= 1_000) { - return numberFormat("en-US").format(value) + return numberFormat(locale).format(value) } return value.toString() } @@ -151,6 +152,8 @@ const KPISection = ({ transactionsToday, className, }: KPISectionProps) => { + const t = useTranslations("page-index") + const locale = useLocale() const { ref: intersectionRef, isIntersecting: isVisible } = useIntersectionObserver({ threshold: 0.3, @@ -169,17 +172,15 @@ const KPISection = ({ >
- The user-owned internet + {t("page-index-kpi-tag")} - Ethereum gives back control of your assets + {t("page-index-kpi-title")}

- Your bank account is an entry in someone else's database. Your - application is a file in someone else's server. Ethereum is an - alternative network where you hold your assets directly. + {t("page-index-kpi-description")}

@@ -194,10 +195,12 @@ const KPISection = ({ />

- {accountHolders !== null ? formatNumber(accountHolders) : "—"} + {accountHolders !== null + ? formatNumber(accountHolders, locale) + : "—"}

- ETH holders + {t("page-index-kpi-holders")}

@@ -218,7 +221,7 @@ const KPISection = ({

)}

- Transactions today + {t("page-index-kpi-transactions")}

diff --git a/src/components/Homepage/PersonaModalCTA.tsx b/src/components/Homepage/PersonaModalCTA.tsx index 69d7723eedf..ba6a32f7e6a 100644 --- a/src/components/Homepage/PersonaModalCTA.tsx +++ b/src/components/Homepage/PersonaModalCTA.tsx @@ -8,6 +8,7 @@ import { Code, ExternalLink, } from "lucide-react" +import { useTranslations } from "next-intl" import { ChevronNext } from "@/components/Chevron" import { Button } from "@/components/ui/buttons/Button" @@ -42,73 +43,91 @@ type PersonaCategory = { links: PersonaLink[] } -const categories: PersonaCategory[] = [ - { - id: "beginners", - label: "For beginners", - Icon: BookOpen, - iconBgClass: "bg-accent-a/20", - iconColorClass: "text-accent-a", - links: [ - { - label: "What is Ethereum?", - href: "/what-is-ethereum/", - eventName: "learn_ethereum", - }, - { - label: "Pick a wallet", - href: "/wallets/find-wallet/", - eventName: "pick_wallet", - }, - ], - }, - { - id: "explorers", - label: "For explorers", - Icon: AppWindowMac, +function useCategories() { + const t = useTranslations("page-index") + + const categories: PersonaCategory[] = [ + { + id: "beginners", + label: t("page-index-modal-beginners"), + Icon: BookOpen, + iconBgClass: "bg-accent-a/20", + iconColorClass: "text-accent-a", + links: [ + { + label: t("page-index-modal-what-is-ethereum"), + href: "/what-is-ethereum/", + eventName: "learn_ethereum", + }, + { + label: t("page-index-modal-pick-wallet"), + href: "/wallets/find-wallet/", + eventName: "pick_wallet", + }, + ], + }, + { + id: "explorers", + label: t("page-index-modal-explorers"), + Icon: AppWindowMac, + iconBgClass: "bg-accent-c/20", + iconColorClass: "text-accent-c", + links: [ + { + label: t("page-index-modal-get-eth"), + href: "/get-eth/", + eventName: "get_eth", + }, + { + label: t("page-index-modal-try-apps"), + href: "/apps/", + eventName: "try_apps", + }, + ], + }, + { + id: "builders", + label: t("page-index-modal-builders"), + Icon: Code, + iconBgClass: "bg-primary-low-contrast", + iconColorClass: "text-primary", + links: [ + { + label: t("page-index-modal-start-building"), + href: "/developers/", + eventName: "start_building", + }, + { + label: t("page-index-modal-docs"), + href: "/developers/docs/", + eventName: "docs", + }, + ], + }, + ] + + const enterpriseCategory: PersonaCategory = { + id: "enterprise", + label: t("page-index-modal-enterprise"), + Icon: Building2, iconBgClass: "bg-accent-c/20", iconColorClass: "text-accent-c", links: [ { - label: "Get ETH", - href: "/get-eth/", - eventName: "get_eth", + label: t("page-index-modal-founders"), + href: "/founders/", + eventName: "founders", }, - { label: "Try apps", href: "/apps/", eventName: "try_apps" }, - ], - }, - { - id: "builders", - label: "For builders", - Icon: Code, - iconBgClass: "bg-primary-low-contrast", - iconColorClass: "text-primary", - links: [ { - label: "Start building", - href: "/developers/", - eventName: "start_building", + label: t("page-index-modal-institutions"), + href: ENTERPRISE_ETHEREUM_URL, + isExternal: true, + eventName: "institutions", }, - { label: "Docs", href: "/developers/docs/", eventName: "docs" }, ], - }, -] - -const enterpriseCategory: PersonaCategory = { - id: "enterprise", - label: "For enterprise", - Icon: Building2, - iconBgClass: "bg-accent-c/20", - iconColorClass: "text-accent-c", - links: [ - { label: "Founders", href: "/founders/", eventName: "founders" }, - { - label: "Institutions", - href: ENTERPRISE_ETHEREUM_URL, - isExternal: true, - eventName: "institutions", - }, - ], + } + + return { categories, enterpriseCategory, t } } const CategoryCard = ({ @@ -172,6 +191,7 @@ type PersonaModalCTAProps = { } const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => { + const { categories, enterpriseCategory, t } = useCategories() const [isOpen, setIsOpen] = useState(false) // Track if modal was closed via link click (not ESC/outside click/X button) const closedViaLinkRef = useRef(false) @@ -215,18 +235,17 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => { - What brings you here? + {t("page-index-modal-title")} - Choose your path: resources for beginners, developers, or - enterprise. + {t("page-index-modal-description")}
diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index 7e269cb7cbe..7ad42755cf5 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react" import { motion, useAnimationControls } from "motion/react" +import { useTranslations } from "next-intl" import type { Swiper as SwiperType } from "swiper" import { SwiperSlide } from "swiper/react" @@ -51,70 +52,75 @@ type Slide = { comparison: ComparisonData } -const slides: Slide[] = [ - { - id: "privacy", - tag: "YOUR BUSINESS IS YOURS", - title: "Use the internet without being watched", - subtitle: - "Most apps track what you do, who you talk to, and what you own. They sell that data or hand it over when asked. On Ethereum, your activity can stay private.", - description: - "No account tied to your name. No company watching your balance.", - cta: "Use privacy preserving apps →", - href: "/apps/categories/privacy/", - image: defiImage, - comparison: { - traditional: { - label: "TRADITIONAL APPS", - value: "Your data is their product", - smallText: true, +function useSlides(): Slide[] { + const t = useTranslations("page-index") + return [ + { + id: "privacy", + tag: t("page-index-carousel-privacy-tag"), + title: t("page-index-carousel-privacy-title"), + subtitle: t("page-index-carousel-privacy-subtitle"), + description: t("page-index-carousel-privacy-description"), + cta: t("page-index-carousel-privacy-cta"), + href: "/apps/categories/privacy/", + image: defiImage, + comparison: { + traditional: { + label: t("page-index-carousel-privacy-traditional-label"), + value: t("page-index-carousel-privacy-traditional-value"), + smallText: true, + }, + ethereum: { + label: t("page-index-carousel-privacy-ethereum-label"), + value: t("page-index-carousel-privacy-ethereum-value"), + smallText: true, + }, }, - ethereum: { - label: "ETHEREUM APPS", - value: "Private by default", - smallText: true, - }, - }, - }, - { - id: "remittances", - tag: "CROSS-BORDER PAYMENTS", - title: "Send money home in 12 minutes", - subtitle: "Skip the $50 wire fee and the 5+ day wait.", - description: - "Send stablecoins to anyone, anywhere in the world, for just $0.02. They receive the funds almost instantly.", - cta: "Try it yourself →", - href: "/payments/", - image: remittancesImage, - comparison: { - traditional: { label: "WIRE TRANSFER", value: "3-5 days" }, - ethereum: { label: "ETHEREUM", value: "12 minutes" }, }, - }, - { - id: "borrowing", - tag: "FINANCIAL ACCESS", - title: "Borrow without credit history", - subtitle: "You don't need a credit score to get started.", - description: - "Using DeFi apps on Ethereum, you can provide collateral and access credit instantly, no permission required.", - cta: "Learn more about DeFi →", - href: "/defi/", - image: borrowingImage, - comparison: { - traditional: { - label: "TRADITIONAL BANK", - value: "Credit checks", - smallText: true, + { + id: "remittances", + tag: t("page-index-carousel-remittances-tag"), + title: t("page-index-carousel-remittances-title"), + subtitle: t("page-index-carousel-remittances-subtitle"), + description: t("page-index-carousel-remittances-description"), + cta: t("page-index-carousel-remittances-cta"), + href: "/payments/", + image: remittancesImage, + comparison: { + traditional: { + label: t("page-index-carousel-remittances-traditional-label"), + value: t("page-index-carousel-remittances-traditional-value"), + }, + ethereum: { + label: t("page-index-carousel-remittances-ethereum-label"), + value: t("page-index-carousel-remittances-ethereum-value"), + }, }, - ethereum: { - label: "ON ETHEREUM", - value: "Based on collateral", - smallText: true, + }, + { + id: "borrowing", + tag: t("page-index-carousel-borrowing-tag"), + title: t("page-index-carousel-borrowing-title"), + subtitle: t("page-index-carousel-borrowing-subtitle"), + description: t("page-index-carousel-borrowing-description"), + cta: t("page-index-carousel-borrowing-cta"), + href: "/defi/", + image: borrowingImage, + comparison: { + traditional: { + label: t("page-index-carousel-borrowing-traditional-label"), + value: t("page-index-carousel-borrowing-traditional-value"), + smallText: true, + }, + ethereum: { + label: t("page-index-carousel-borrowing-ethereum-label"), + value: t("page-index-carousel-borrowing-ethereum-value"), + smallText: true, + }, }, }, - }, -] + ] +} type SavingsCarouselProps = { className?: string @@ -298,6 +304,7 @@ const SavingsCarousel = ({ className, eventCategory = "Homepage", }: SavingsCarouselProps) => { + const slides = useSlides() const [activeIndex, setActiveIndex] = useState(0) const handleSlideChange = (swiper: SwiperType) => { diff --git a/src/components/Homepage/SimulatorSection/index.tsx b/src/components/Homepage/SimulatorSection/index.tsx index b117ed55668..01a042a9285 100644 --- a/src/components/Homepage/SimulatorSection/index.tsx +++ b/src/components/Homepage/SimulatorSection/index.tsx @@ -1,6 +1,7 @@ "use client" import { useEffect, useState } from "react" +import { useTranslations } from "next-intl" import { useIntersectionObserver } from "usehooks-ts" import { SEND_RECEIVE } from "@/components/Simulator/constants" @@ -30,6 +31,7 @@ const SimulatorSkeleton = () => ( const sendReceiveData = walletOnboardingSimData[SEND_RECEIVE] const SimulatorSection = ({ className }: SimulatorSectionProps) => { + const t = useTranslations("page-index") const { ref: sectionRef, isIntersecting: isVisible } = useIntersectionObserver({ rootMargin: "200px", @@ -64,12 +66,12 @@ const SimulatorSection = ({ className }: SimulatorSectionProps) => { className={cn("flex flex-col items-center gap-8", className)} >
- Free forever + {t("page-index-simulator-tag")} - Try Ethereum in your browser + {t("page-index-simulator-title")}

- Experience how Ethereum works. Just click and explore. + {t("page-index-simulator-subtitle")}

diff --git a/src/components/Homepage/TrustLogos.tsx b/src/components/Homepage/TrustLogos.tsx index 0da84e0c26f..322809e9e54 100644 --- a/src/components/Homepage/TrustLogos.tsx +++ b/src/components/Homepage/TrustLogos.tsx @@ -1,4 +1,5 @@ import { ArrowRight, Check } from "lucide-react" +import { getTranslations } from "next-intl/server" import { Image } from "@/components/Image" import { BaseLink } from "@/components/ui/Link" @@ -20,10 +21,11 @@ type TrustLogosProps = { eventCategory?: string } -const TrustLogos = ({ +const TrustLogos = async ({ className, eventCategory = "Homepage", }: TrustLogosProps) => { + const t = await getTranslations("page-index") return (
Ethereum community illustration

- Never offline + {t("page-index-trust-never-offline")}

- 100% uptime + {t("page-index-trust-uptime")}

- 10 years + {t("page-index-trust-years")}

- Since 2015 + {t("page-index-trust-since")}

@@ -67,20 +69,15 @@ const TrustLogos = ({
- Proven track record - Built to last + {t("page-index-trust-tag")} + + {t("page-index-trust-title")} +
-

- Ethereum has run continuously since 2015 without a single second of - downtime. -

-

- The code is open for anyone to verify. No company runs it, no one - can shut it down, and thousands of independent operators keep it - going worldwide. -

+

{t("page-index-trust-description-1")}

+

{t("page-index-trust-description-2")}

- Get ETH + {t("page-index-trust-cta")}
diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index fac8cdf19dc..c35466e1899 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -1,140 +1,116 @@ { - "page-index-activity-description": "Ethereum is the leading platform for issuing, managing, and settling digital assets. From tokenized money and financial instruments to real-world assets and emerging markets, Ethereum provides a secure, neutral foundation for the digital economy.", - "page-index-activity-subtitle": "Activity on Ethereum Mainnet and Layer 2 networks", - "page-index-activity-tag": "Activity", - "page-index-activity-header": "The strongest ecosystem", - "page-index-activity-action": "More ecosystem resources", - "page-index-activity-action-primary": "Ethereum for institutions", - "page-index-bento-header": "A new way to use the internet", - "page-index-bento-assets-action": "More on NFTs", - "page-index-bento-assets-content": "From art to real estate to stocks, any asset can be tokenized on Ethereum to prove and verify ownership digitally. Buy, sell, trade, and create assets and collectibles—anytime, anywhere.", - "page-index-bento-assets-title": "The internet of assets", - "page-index-bento-dapps-action": "Browse apps", - "page-index-bento-dapps-content": "Apps built on Ethereum work without selling your data. From social media to gaming to work, use the same account for every innovative app while maintaining privacy and access.", - "page-index-bento-dapps-title": "Apps that respect your privacy", - "page-index-bento-defi-action": "Explore DeFi", - "page-index-bento-defi-content": "Borrow, lend, earn interest, and more, without a bank account. Ethereum's decentralized financial system is open 24/7 to anyone with an internet connection.", - "page-index-bento-defi-title": "A financial system open to all", - "page-index-bento-networks-action": "Discover Layer 2s", - "page-index-bento-networks-content": "Hundreds of Layer 2 networks are built on Ethereum. Enjoy low fees and near-instant transactions while benefiting from Ethereum's proven security.", - "page-index-bento-networks-title": "The network of networks", - "page-index-bento-stablecoins-action": "Discover stablecoins", - "page-index-bento-stablecoins-content": "Stablecoins are digital currencies that maintain a stable price that is matched to steady assets like the U.S. dollar. They facilitate instant global payments or store value in digital dollars on Ethereum.", - "page-index-bento-stablecoins-title": "Digital cash for everyday use", - "page-index-builders-action-primary": "Builder's portal", - "page-index-builders-action-secondary": "Documentation", - "page-index-builders-description": "Ethereum is home to Web3's largest and most vibrant developer ecosystem. Use JavaScript and Python, or learn a smart contract language like Solidity or Vyper to write your own app.", - "page-index-builders-tag": "Builders", - "page-index-builders-header": "Blockchain's biggest builder community", - "page-index-calendar-add": "Add to calendar", - "page-index-calendar-fallback": "No upcoming calls", - "page-index-calendar-title": "Next calls", - "page-index-community-action": "More on ethereum.org", - "page-index-community-description-1": "The ethereum.org website is built and maintained by hundreds of translators, coders, designers, copywriters, and enthusiastic community members each month.", - "page-index-community-description-2": "Come ask questions, connect with people around the world and contribute to the website. You will get relevant practical experience and be guided during the process!", - "page-index-community-description-3": "Ethereum.org community is the perfect place to start and learn.", - "page-index-community-tag": "Ethereum.org Community", - "page-index-community-header": "Built by the community", - "page-index-cta-dapps-description": "Finance, gaming, social", - "page-index-cta-dapps-label": "Try apps", - "page-index-cta-get-eth-description": "The currency of Ethereum", - "page-index-cta-get-eth-label": "Get ETH", - "page-index-cta-wallet-description": "Create accounts & manage assets", - "page-index-cta-wallet-label": "Pick a wallet", - "page-index-cta-build-apps-description": "Create your first app", - "page-index-cta-build-apps-label": "Start building", - "page-index-description": "The leading platform for innovative apps and blockchain networks", - "page-index-developers-code-example-description-0": "Build a bank powered by logic you've programmed", - "page-index-developers-code-example-description-1": "Create tokens that you can transfer and use across applications", - "page-index-developers-code-example-description-2": "Use existing languages to interact with Ethereum and other applications", - "page-index-developers-code-example-description-3": "Re-imagine existing services as decentralized, open applications", - "page-index-developers-code-example-title-0": "Your own bank", - "page-index-developers-code-example-title-1": "Your own currency", - "page-index-developers-code-example-title-2": "A JavaScript Ethereum wallet", - "page-index-developers-code-example-title-3": "An open, permissionless DNS", - "page-index-developers-code-examples": "Code examples", - "page-index-events-action": "See all events", - "page-index-events-header": "Ethereum events", - "page-index-events-subtitle": "Ethereum communities host events all around the globe, all year long", - "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.", - "page-index-join-action-contribute-description": "Find out all the different ways you can help ethereum.org grow and be better.", - "page-index-join-action-contribute-label": "How to contribute", - "page-index-join-action-discord-description": "To ask questions, coordinate contribution and join community calls.", - "page-index-join-action-github-description": "Contribute to code, design, articles, etc.", - "page-index-join-action-twitter-description": "To keep up with our updates and important news.", - "page-index-join-description": "The ethereum.org website is built and maintained by thousands of translators, coders, designers, copywriters, and community members. You can propose edits to any of the content on this open source site.", - "page-index-join-header": "Join ethereum.org", - "page-index-join-action-hub": "ethereum.org contributor hub", - "page-index-learn-description": "Ethereum is a decentralized blockchain network and software development platform, powered by the cryptocurrency ether (ETH). These resources are your gateway to confidently navigate, understand, and use Ethereum.", - "page-index-what-is-ethereum-title": "What is Ethereum?", - "page-index-what-is-ethereum-description-1": "Ethereum is a decentralized, open source blockchain network and software development platform, powered by the cryptocurrency ether (ETH). Ethereum is the secure, global foundation for a new generation of unstoppable applications.", - "page-index-what-is-ethereum-description-2": "The Ethereum network is open to everyone: no permission is required. It has no owner, and is built and maintained by thousands of people, organizations, and users around the world.", - "page-index-what-is-ethereum-action": "Learn about Ethereum", - "page-index-what-is-ether-title": "What is ETH?", - "page-index-what-is-ether-description-1": "Ether (ETH) is the native cryptocurrency that powers the Ethereum network, used to pay transaction fees and secure the blockchain through staking.", - "page-index-what-is-ether-description-2": "Beyond its technical role, ETH is open, programmable digital money. It is used for global payments, as collateral for loans, and as a store of value that doesn't rely on any central entity.", - "page-index-what-is-ether-action": "Learn more about ether", - "page-index-learn-tag": "Learn", - "page-index-network-tag": "Network", - "page-index-token-tag": "Token", - "page-index-learn-header": "Understand Ethereum", - "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.", "page-index-meta-title": "Ethereum.org: The complete guide to Ethereum", - "page-index-network-stats-total-eth-staked": "Value protecting Ethereum", - "page-index-network-stats-tx-cost-description": "Average transaction cost", - "page-index-network-stats-tx-day-description": "Transactions in the last 24h", - "page-index-network-stats-value-defi-description": "Value locked in DeFi", - "page-index-network-stats-total-value-held": "Total value held on Ethereum", - "page-index-popular-topics-ethereum": "What is Ethereum?", - "page-index-popular-topics-header": "Popular topics", - "page-index-popular-topics-action": "More guides in Ethereum Learn Hub", - "page-index-popular-topics-roadmap": "Ethereum roadmap", - "page-index-popular-topics-start": "Step-by-step Ethereum guides", - "page-index-popular-topics-wallets": "What are crypto wallets?", - "page-index-popular-topics-whitepaper": "Ethereum Whitepaper", - "page-index-posts-action": "Read more on these websites", - "page-index-posts-header": "Ethereum news", - "page-index-posts-subtitle": "The latest blog posts and updates from the community", + "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.", + "page-index-description": "The leading platform for innovative apps and blockchain networks", "page-index-title": "Welcome to Ethereum", - "page-index-use-cases-tag": "Use cases", - "page-index-values-description": "Be part of the digital revolution", - "page-index-values-header": "The internet is changing", - "page-index-values-legacy": "Legacy", - "page-index-values-tag": "Values", - "page-index-values-ownership-legacy-label": "Restricted ownership", - "page-index-values-ownership-legacy-content-0": "With a regular bank or social media platform, your assets and data are managed by the organization. You rely on them for access and control.", - "page-index-values-ownership-legacy-content-1": "They may use your data in ways you might not agree with, based on their policies.", - "page-index-values-ownership-ethereum-label": "Direct ownership", - "page-index-values-ownership-ethereum-content-0": "With Ethereum, only you have access and control. Nobody else should ever be able to use your assets. You can decide who to grant that permission.", - "page-index-values-fairness-legacy-label": "Discriminatory", - "page-index-values-fairness-legacy-content-0": "Today, not everyone has the same access to financial services. Some people may face barriers to access due to their location or nationality.", - "page-index-values-fairness-ethereum-label": "Equal Access", - "page-index-values-fairness-ethereum-content-0": "We believe everyone should be allowed to benefit from a global system. That is why Ethereum grants equal access to all worldwide, regardless of who you are or where you come from.", - "page-index-values-privacy-legacy-label": "No privacy", - "page-index-values-privacy-legacy-content-0": "We cannot expect governments, corporations, or other large, faceless organizations to grant us privacy out of their beneficence.", - "page-index-values-privacy-legacy-content-1": "Most apps gather as much of your personal information as possible so that they can target you with tailored marketing.", - "page-index-values-privacy-ethereum-label": "Privacy oriented", - "page-index-values-privacy-ethereum-content-0": "Ethereum community respects privacy. You have the right to use apps without revealing yourself or your contact information.", - "page-index-values-integration-legacy-label": "Fragmented", - "page-index-values-integration-legacy-content-0": "Most apps push you to create separate accounts, making it hard to remember all your login details and registrations.", - "page-index-values-integration-ethereum-label": "Integrated", - "page-index-values-integration-ethereum-content-0": "With Ethereum you can reuse one account in all apps instead. No individual registrations are needed.", - "page-index-values-decentralization-legacy-label": "Centralized", - "page-index-values-decentralization-legacy-content-0": "Companies are owned by private entrepreneurs and shareholders. They alone exert control over the company and benefit the most from its success.", - "page-index-values-decentralization-ethereum-label": "Decentralized", - "page-index-values-decentralization-ethereum-content-0": "Just like the internet itself, Ethereum doesn't belong to anyone. It’s shared and shaped equally with all. There is no single owner who could control it.", - "page-index-values-censorship-legacy-label": "Censorable", - "page-index-values-censorship-legacy-content-0": "Modern platforms and its rules often change. They can be influenced by stakeholders, company management or even oppressive regimes.", - "page-index-values-censorship-ethereum-label": "Censorship resistant", - "page-index-values-censorship-ethereum-content-0": "Resistance to oppression is a key principle of Ethereum. Its functionality should always stay fair and impartial.", - "page-index-values-censorship-ethereum-content-1": "Ethereum cannot be controlled by any nation state, company or individual.", - "page-index-values-open-legacy-label": "Closed to most", - "page-index-values-open-legacy-content-0": "Companies protect their intellectual property and don’t share. No one outside the company can see how things work, fix problems, or make improvements. It's hard for people to create new tools or customize.", - "page-index-values-open-ethereum-label": "Open to all", - "page-index-values-open-ethereum-content-0": "Ethereum is public to all. Anyone can see, use, and improve the code, making it better for everyone.", - "page-index-fusaka-network-upgrade": "Network upgrade", - "page-index-fusaka-description": "For a faster, safer, and more user-friendly Ethereum network |", - "page-index-fusaka-read-more": "Read more", - "page-index-fusaka-going-live-in": "Going

live in", - "page-index-fusaka-live-now": "Live now" -} \ No newline at end of file + "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.", + "page-index-hero-title": "The internet that belongs to you", + "page-index-hero-subtitle": "Ethereum is the global network where you control your assets, your data, and your identity.", + "page-index-hero-cta": "Start here", + "page-index-cta-wallet-label": "Pick a wallet", + "page-index-cta-wallet-description": "Create accounts & manage assets", + "page-index-cta-get-eth-label": "Get ETH", + "page-index-cta-get-eth-description": "The currency of Ethereum", + "page-index-cta-dapps-label": "Try apps", + "page-index-cta-dapps-description": "See what Ethereum can do", + "page-index-cta-build-apps-label": "Start building", + "page-index-cta-build-apps-description": "Create your first app", + "page-index-cta-learn-label": "Learn Ethereum", + "page-index-modal-title": "What brings you here?", + "page-index-modal-description": "Choose your path: resources for beginners, developers, or enterprise.", + "page-index-modal-beginners": "For beginners", + "page-index-modal-explorers": "For explorers", + "page-index-modal-builders": "For builders", + "page-index-modal-enterprise": "For enterprise", + "page-index-modal-what-is-ethereum": "What is Ethereum?", + "page-index-modal-pick-wallet": "Pick a wallet", + "page-index-modal-get-eth": "Get ETH", + "page-index-modal-try-apps": "Try apps", + "page-index-modal-start-building": "Start building", + "page-index-modal-docs": "Docs", + "page-index-modal-founders": "Founders", + "page-index-modal-institutions": "Institutions", + "page-index-kpi-tag": "The user-owned internet", + "page-index-kpi-title": "Ethereum gives back control of your assets", + "page-index-kpi-description": "Your bank account is an entry in someone else's database. Your application is a file in someone else's server. Ethereum is an alternative network where you hold your assets directly.", + "page-index-kpi-holders": "ETH holders", + "page-index-kpi-transactions": "Transactions today", + "page-index-carousel-privacy-tag": "YOUR BUSINESS IS YOURS", + "page-index-carousel-privacy-title": "Use the internet without being watched", + "page-index-carousel-privacy-subtitle": "Most apps track what you do, who you talk to, and what you own. They sell that data or hand it over when asked. On Ethereum, your activity can stay private.", + "page-index-carousel-privacy-description": "No account tied to your name. No company watching your balance.", + "page-index-carousel-privacy-cta": "Use privacy preserving apps →", + "page-index-carousel-privacy-traditional-label": "TRADITIONAL APPS", + "page-index-carousel-privacy-traditional-value": "Your data is their product", + "page-index-carousel-privacy-ethereum-label": "ETHEREUM APPS", + "page-index-carousel-privacy-ethereum-value": "Private by default", + "page-index-carousel-remittances-tag": "CROSS-BORDER PAYMENTS", + "page-index-carousel-remittances-title": "Send money home in 12 minutes", + "page-index-carousel-remittances-subtitle": "Skip the $50 wire fee and the 5+ day wait.", + "page-index-carousel-remittances-description": "Send stablecoins to anyone, anywhere in the world, for just $0.02. They receive the funds almost instantly.", + "page-index-carousel-remittances-cta": "Try it yourself →", + "page-index-carousel-remittances-traditional-label": "WIRE TRANSFER", + "page-index-carousel-remittances-traditional-value": "3-5 days", + "page-index-carousel-remittances-ethereum-label": "ETHEREUM", + "page-index-carousel-remittances-ethereum-value": "12 minutes", + "page-index-carousel-borrowing-tag": "FINANCIAL ACCESS", + "page-index-carousel-borrowing-title": "Borrow without credit history", + "page-index-carousel-borrowing-subtitle": "You don't need a credit score to get started.", + "page-index-carousel-borrowing-description": "Using DeFi apps on Ethereum, you can provide collateral and access credit instantly, no permission required.", + "page-index-carousel-borrowing-cta": "Learn more about DeFi →", + "page-index-carousel-borrowing-traditional-label": "TRADITIONAL BANK", + "page-index-carousel-borrowing-traditional-value": "Credit checks", + "page-index-carousel-borrowing-ethereum-label": "ON ETHEREUM", + "page-index-carousel-borrowing-ethereum-value": "Based on collateral", + "page-index-trust-image-alt": "Ethereum community illustration", + "page-index-trust-never-offline": "Never offline", + "page-index-trust-uptime": "100% uptime", + "page-index-trust-years": "10 years", + "page-index-trust-since": "Since 2015", + "page-index-trust-tag": "Proven track record", + "page-index-trust-title": "Built to last", + "page-index-trust-description-1": "Ethereum has run continuously since 2015 without a single second of downtime.", + "page-index-trust-description-2": "The code is open for anyone to verify. No company runs it, no one can shut it down, and thousands of independent operators keep it going worldwide.", + "page-index-trust-cta": "Get ETH", + "page-index-simulator-tag": "Free forever", + "page-index-simulator-title": "Try Ethereum in your browser", + "page-index-simulator-subtitle": "Experience how Ethereum works. Just click and explore.", + "page-index-features-title": "What makes Ethereum", + "page-index-features-title-highlight": "different", + "page-index-features-subtitle": "Principles that set Ethereum apart from traditional systems", + "page-index-features-ownership-title": "Direct ownership", + "page-index-features-ownership-description-1": "Your bank balance is a ", + "page-index-features-ownership-description-custody": "custody promise", + "page-index-features-ownership-description-2": "Your Ethereum balance is true ownership.", + "page-index-features-ownership-stat": "4.6B+", + "page-index-features-ownership-stat-label": "Daily trading volume", + "page-index-features-public-rules-title": "Public rules", + "page-index-features-public-rules-description": "The code is public, agreements execute exactly as written. Think vending machine versus hoping the cashier gives correct change.", + "page-index-features-global-title": "Global", + "page-index-features-global-description": "Anyone, anywhere can use Ethereum. No permission needed.", + "page-index-features-free-access-title": "Free access", + "page-index-features-free-access-description": "No credit check, no minimum balance, no account approval. If you have internet, you're in.", + "page-index-features-nobody-owns-title": "Nobody owns Ethereum", + "page-index-features-nobody-owns-description": "Changes happen through open proposals that anyone can participate in. Think community garden versus corporate farm.", + "page-index-features-cta": "What is Ethereum?", + "page-index-get-started-title": "Get started on Ethereum", + "page-index-get-started-subtitle": "Takes 2 minutes to get started. No credit check, no paperwork, no minimum balance.", + "page-index-get-started-learn-title": "Understand Ethereum", + "page-index-get-started-learn-description": "Start here. Learn what it is, why it matters, and how it works in plain language.", + "page-index-get-started-learn-bullet-1": "What is Ethereum?", + "page-index-get-started-learn-bullet-2": "How do wallets work?", + "page-index-get-started-learn-bullet-3": "DeFi, stablecoins, and NFTs explained", + "page-index-get-started-learn-cta": "Start learning", + "page-index-get-started-build-title": "Start building", + "page-index-get-started-build-description": "For developers. Access documentation, tools, and tutorials to build on Ethereum.", + "page-index-get-started-build-bullet-1": "Developer documentation", + "page-index-get-started-build-bullet-2": "Smart contract tutorials", + "page-index-get-started-build-bullet-3": "Development tools & frameworks", + "page-index-get-started-build-cta": "View materials", + "page-index-get-started-enterprise-title": "For enterprise", + "page-index-get-started-enterprise-description": "Business use cases, institutional resources, and how Ethereum can serve your organization.", + "page-index-get-started-enterprise-bullet-1": "Enterprise use cases", + "page-index-get-started-enterprise-bullet-2": "Private & permissioned networks", + "page-index-get-started-enterprise-bullet-3": "Institutional resources", + "page-index-get-started-enterprise-cta": "Explore enterprise" +} From 40248c92e8f5ffa901f53a9f35b075b58232d084 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 15 Apr 2026 20:40:07 +0200 Subject: [PATCH 006/109] fix: provide page-index translations to client components via i18n provider --- src/components/Homepage/Homepage2026.tsx | 112 +++++++++++------- .../Homepage/SimulatorSection/index.tsx | 17 +-- 2 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/components/Homepage/Homepage2026.tsx b/src/components/Homepage/Homepage2026.tsx index f0683f2e86b..9657ad09c38 100644 --- a/src/components/Homepage/Homepage2026.tsx +++ b/src/components/Homepage/Homepage2026.tsx @@ -1,5 +1,7 @@ import { Suspense } from "react" +import { pick } from "lodash" import dynamic from "next/dynamic" +import { getMessages, getTranslations } from "next-intl/server" import type { Lang } from "@/lib/types" @@ -8,9 +10,10 @@ import FeatureCards from "@/components/Homepage/FeatureCards" import GetStartedGrid from "@/components/Homepage/GetStartedGrid" import { SimulatorI18nWrapper } from "@/components/Homepage/SimulatorSection/SimulatorI18nWrapper" import TrustLogos from "@/components/Homepage/TrustLogos" +import I18nProvider from "@/components/I18nProvider" import MainArticle from "@/components/MainArticle" import { TrackedSection } from "@/components/TrackedSection" -import { Section } from "@/components/ui/section" +import { Section, SectionHeader, SectionTag } from "@/components/ui/section" import { getDirection } from "@/lib/utils/direction" @@ -36,58 +39,81 @@ type Homepage2026Props = { ctaVariant?: CTAVariant } -const Homepage2026 = ({ +const Homepage2026 = async ({ locale, accountHolders, transactionsToday, ctaVariant = "modal", }: Homepage2026Props) => { const { direction: dir } = getDirection(locale) + const t = await getTranslations("page-index") + const allMessages = await getMessages() + const messages = pick(allMessages, "page-index") const eventCategory = `Homepage - ${locale}` return ( - - - -
- - }> - - - - - - }> - - - - - - - - - - }> - - - - - - - - - - - - - -
-
+ + + + +
+ + }> + + + + + + }> + + + + + + + + + + }> + + + + {t("page-index-simulator-tag")} + + + {t("page-index-simulator-title")} + +

+ {t("page-index-simulator-subtitle")} +

+
+ } + /> + + + + + + + + + + + + +
+
) } diff --git a/src/components/Homepage/SimulatorSection/index.tsx b/src/components/Homepage/SimulatorSection/index.tsx index 01a042a9285..b548b10d22d 100644 --- a/src/components/Homepage/SimulatorSection/index.tsx +++ b/src/components/Homepage/SimulatorSection/index.tsx @@ -1,7 +1,6 @@ "use client" import { useEffect, useState } from "react" -import { useTranslations } from "next-intl" import { useIntersectionObserver } from "usehooks-ts" import { SEND_RECEIVE } from "@/components/Simulator/constants" @@ -9,7 +8,7 @@ import { Explanation } from "@/components/Simulator/Explanation" import type { SimulatorNav } from "@/components/Simulator/interfaces" import { Phone } from "@/components/Simulator/Phone" import { Template } from "@/components/Simulator/Template" -import { Section, SectionHeader, SectionTag } from "@/components/ui/section" +import { Section } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" @@ -17,6 +16,7 @@ import { walletOnboardingSimData } from "@/data/WalletSimulatorData" type SimulatorSectionProps = { className?: string + header?: React.ReactNode } /** @@ -30,8 +30,7 @@ const SimulatorSkeleton = () => ( const sendReceiveData = walletOnboardingSimData[SEND_RECEIVE] -const SimulatorSection = ({ className }: SimulatorSectionProps) => { - const t = useTranslations("page-index") +const SimulatorSection = ({ className, header }: SimulatorSectionProps) => { const { ref: sectionRef, isIntersecting: isVisible } = useIntersectionObserver({ rootMargin: "200px", @@ -65,15 +64,7 @@ const SimulatorSection = ({ className }: SimulatorSectionProps) => { ref={sectionRef} className={cn("flex flex-col items-center gap-8", className)} > -
- {t("page-index-simulator-tag")} - - {t("page-index-simulator-title")} - -

- {t("page-index-simulator-subtitle")} -

-
+ {header}
{!isVisible || !isLoaded ? ( From a1f876f8f88aeaab47b7fb1be5379babe01eabdd Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 15 Apr 2026 20:52:05 +0200 Subject: [PATCH 007/109] perf: lazy load persona modal to defer radix dialog from initial bundle --- src/components/Hero/HomeHero2026/index.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/Hero/HomeHero2026/index.tsx b/src/components/Hero/HomeHero2026/index.tsx index ba99ce10030..0ef7ec6e032 100644 --- a/src/components/Hero/HomeHero2026/index.tsx +++ b/src/components/Hero/HomeHero2026/index.tsx @@ -1,11 +1,17 @@ -import { Fragment } from "react" +import { Fragment, Suspense } from "react" +import dynamic from "next/dynamic" import { getImageProps, type StaticImageData } from "next/image" import { getTranslations } from "next-intl/server" import type { ClassNameProp } from "@/lib/types" +import { ChevronNext } from "@/components/Chevron" import LanguageMorpher from "@/components/Homepage/LanguageMorpher" -import PersonaModalCTA from "@/components/Homepage/PersonaModalCTA" +import { Button } from "@/components/ui/buttons/Button" + +const PersonaModalCTA = dynamic( + () => import("@/components/Homepage/PersonaModalCTA") +) import EthGlyphIcon from "@/components/icons/eth-glyph.svg" import EthTokenIcon from "@/components/icons/eth-token.svg" import EthWalletIcon from "@/components/icons/eth-wallet.svg" @@ -135,7 +141,16 @@ const HomeHero2026 = async ({

{ctaVariant === "modal" ? ( - + + {t("page-index-hero-cta")} + + + } + > + + ) : (
{directButtonCTAs.map( From 5b04036608dea4d76204b9676a676d5594892a1e Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 11:14:10 +0200 Subject: [PATCH 008/109] refactor: keep simulator section with header prop, revert wrapper to simple form --- .../Homepage/SimulatorSection/SimulatorI18nWrapper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx b/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx index 7ae01b5aefd..97fe6ffbed5 100644 --- a/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx +++ b/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx @@ -5,8 +5,9 @@ import { NextIntlClientProvider } from "next-intl" /** * Minimal glossary translations for the SimulatorSection. * - * The homepage is English-only, so we hardcode just the required translations - * instead of loading the full glossary-tooltip namespace. + * The Simulator components use glossary-tooltip keys internally. + * We hardcode just the required translations instead of loading + * the full glossary-tooltip namespace. */ const SIMULATOR_MESSAGES = { "glossary-tooltip": { From ed00f530b1b675fee8020a38eb2d4d5061e33d43 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 11:19:10 +0200 Subject: [PATCH 009/109] refactor: remove simulator i18n wrapper, pick only needed glossary keys --- src/components/Homepage/Homepage2026.tsx | 46 +++++++++++-------- .../SimulatorSection/SimulatorI18nWrapper.tsx | 33 ------------- 2 files changed, 26 insertions(+), 53 deletions(-) delete mode 100644 src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx diff --git a/src/components/Homepage/Homepage2026.tsx b/src/components/Homepage/Homepage2026.tsx index 9657ad09c38..5ec65513d33 100644 --- a/src/components/Homepage/Homepage2026.tsx +++ b/src/components/Homepage/Homepage2026.tsx @@ -8,7 +8,6 @@ import type { Lang } from "@/lib/types" import HomeHero2026, { type CTAVariant } from "@/components/Hero/HomeHero2026" import FeatureCards from "@/components/Homepage/FeatureCards" import GetStartedGrid from "@/components/Homepage/GetStartedGrid" -import { SimulatorI18nWrapper } from "@/components/Homepage/SimulatorSection/SimulatorI18nWrapper" import TrustLogos from "@/components/Homepage/TrustLogos" import I18nProvider from "@/components/I18nProvider" import MainArticle from "@/components/MainArticle" @@ -48,7 +47,16 @@ const Homepage2026 = async ({ const { direction: dir } = getDirection(locale) const t = await getTranslations("page-index") const allMessages = await getMessages() - const messages = pick(allMessages, "page-index") + const glossary = allMessages["glossary-tooltip"] as Record + const messages = { + ...pick(allMessages, "page-index"), + "glossary-tooltip": pick(glossary, [ + "nft-term", + "nft-definition", + "web3-term", + "web3-definition", + ]), + } const eventCategory = `Homepage - ${locale}` @@ -83,24 +91,22 @@ const Homepage2026 = async ({ }> - - - - {t("page-index-simulator-tag")} - - - {t("page-index-simulator-title")} - -

- {t("page-index-simulator-subtitle")} -

-
- } - /> - + + + {t("page-index-simulator-tag")} + + + {t("page-index-simulator-title")} + +

+ {t("page-index-simulator-subtitle")} +

+
+ } + /> diff --git a/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx b/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx deleted file mode 100644 index 97fe6ffbed5..00000000000 --- a/src/components/Homepage/SimulatorSection/SimulatorI18nWrapper.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client" - -import { NextIntlClientProvider } from "next-intl" - -/** - * Minimal glossary translations for the SimulatorSection. - * - * The Simulator components use glossary-tooltip keys internally. - * We hardcode just the required translations instead of loading - * the full glossary-tooltip namespace. - */ -const SIMULATOR_MESSAGES = { - "glossary-tooltip": { - "nft-term": "Non-fungible token (NFT)", - "nft-definition": - 'A unique digital item you can own, like art or collectibles, verified by blockchain technology. More on non-fungible tokens (NFTs).', - "web3-term": "Web3", - "web3-definition": - 'Web3 is the new internet with blockchain, where users control their data and transactions, not companies. No need to share any personal information. More on web3.', - }, -} - -export function SimulatorI18nWrapper({ - children, -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) -} From 3bb3483135a3f8aec81435988cf55d1cd6417510 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 11:31:47 +0200 Subject: [PATCH 010/109] refactor: inline homepage2026 wrapper into page.tsx --- app/[locale]/page.tsx | 113 ++++++++++++++++++-- src/components/Homepage/Homepage2026.tsx | 126 ----------------------- 2 files changed, 106 insertions(+), 133 deletions(-) delete mode 100644 src/components/Homepage/Homepage2026.tsx diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 5f5c44b87ba..b60a7fb3a23 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,10 +1,25 @@ +import { Suspense } from "react" +import { pick } from "lodash" +import dynamic from "next/dynamic" import { notFound } from "next/navigation" -import { getTranslations, setRequestLocale } from "next-intl/server" +import { + getMessages, + getTranslations, + setRequestLocale, +} from "next-intl/server" import type { PageParams } from "@/lib/types" -import Homepage2026 from "@/components/Homepage/Homepage2026" +import HomeHero2026 from "@/components/Hero/HomeHero2026" +import FeatureCards from "@/components/Homepage/FeatureCards" +import GetStartedGrid from "@/components/Homepage/GetStartedGrid" +import TrustLogos from "@/components/Homepage/TrustLogos" +import I18nProvider from "@/components/I18nProvider" +import MainArticle from "@/components/MainArticle" +import { TrackedSection } from "@/components/TrackedSection" +import { Section, SectionHeader, SectionTag } from "@/components/ui/section" +import { getDirection } from "@/lib/utils/direction" import { getMetadata } from "@/lib/utils/metadata" import { DEFAULT_LOCALE, LOCALES_CODES } from "@/lib/constants" @@ -13,6 +28,20 @@ import IndexPageJsonLD from "./page-jsonld" import { getAccountHolders, getGrowThePieData } from "@/lib/data" +const KPISection = dynamic(() => import("@/components/Homepage/KPISection")) +const SavingsCarousel = dynamic( + () => import("@/components/Homepage/SavingsCarousel") +) +const SimulatorSection = dynamic( + () => import("@/components/Homepage/SimulatorSection") +) + +const SectionSkeleton = ({ className }: { className?: string }) => ( +
+
+
+) + const Page = async (props: { params: Promise }) => { const params = await props.params const { locale } = params @@ -39,14 +68,84 @@ const Page = async (props: { params: Promise }) => { const transactionsToday = "value" in growThePieData.txCount ? growThePieData.txCount.value : null + const { direction: dir } = getDirection(locale) + const t = await getTranslations("page-index") + const allMessages = await getMessages() + const glossary = allMessages["glossary-tooltip"] as Record + const messages = { + ...pick(allMessages, "page-index"), + "glossary-tooltip": pick(glossary, [ + "nft-term", + "nft-definition", + "web3-term", + "web3-definition", + ]), + } + + const eventCategory = `Homepage - ${locale}` + return ( <> - + + + + +
+ + }> + + + + + + }> + + + + + + + + + + }> + + + {t("page-index-simulator-tag")} + + + {t("page-index-simulator-title")} + +

+ {t("page-index-simulator-subtitle")} +

+
+ } + /> + + + + + + + + + + + +
+
) } diff --git a/src/components/Homepage/Homepage2026.tsx b/src/components/Homepage/Homepage2026.tsx deleted file mode 100644 index 5ec65513d33..00000000000 --- a/src/components/Homepage/Homepage2026.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Suspense } from "react" -import { pick } from "lodash" -import dynamic from "next/dynamic" -import { getMessages, getTranslations } from "next-intl/server" - -import type { Lang } from "@/lib/types" - -import HomeHero2026, { type CTAVariant } from "@/components/Hero/HomeHero2026" -import FeatureCards from "@/components/Homepage/FeatureCards" -import GetStartedGrid from "@/components/Homepage/GetStartedGrid" -import TrustLogos from "@/components/Homepage/TrustLogos" -import I18nProvider from "@/components/I18nProvider" -import MainArticle from "@/components/MainArticle" -import { TrackedSection } from "@/components/TrackedSection" -import { Section, SectionHeader, SectionTag } from "@/components/ui/section" - -import { getDirection } from "@/lib/utils/direction" - -// Heavy client components loaded dynamically for better code splitting -const KPISection = dynamic(() => import("@/components/Homepage/KPISection")) -const SavingsCarousel = dynamic( - () => import("@/components/Homepage/SavingsCarousel") -) -const SimulatorSection = dynamic( - () => import("@/components/Homepage/SimulatorSection") -) - -const SectionSkeleton = ({ className }: { className?: string }) => ( -
-
-
-) - -type Homepage2026Props = { - locale: Lang - accountHolders: number | null - transactionsToday: number | null - ctaVariant?: CTAVariant -} - -const Homepage2026 = async ({ - locale, - accountHolders, - transactionsToday, - ctaVariant = "modal", -}: Homepage2026Props) => { - const { direction: dir } = getDirection(locale) - const t = await getTranslations("page-index") - const allMessages = await getMessages() - const glossary = allMessages["glossary-tooltip"] as Record - const messages = { - ...pick(allMessages, "page-index"), - "glossary-tooltip": pick(glossary, [ - "nft-term", - "nft-definition", - "web3-term", - "web3-definition", - ]), - } - - const eventCategory = `Homepage - ${locale}` - - return ( - - - - -
- - }> - - - - - - }> - - - - - - - - - - }> - - - {t("page-index-simulator-tag")} - - - {t("page-index-simulator-title")} - -

- {t("page-index-simulator-subtitle")} -

-
- } - /> - - - - - - - - - - - -
-
- ) -} - -export default Homepage2026 From a604f9845ae16fa75c0bb000724045bd368df78c Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 11:48:20 +0200 Subject: [PATCH 011/109] i18n: replace hardcoded numbers and currency with locale-aware numberformat --- src/components/Homepage/FeatureCards.tsx | 11 ++++-- src/components/Homepage/GetStartedGrid.tsx | 8 +++-- src/components/Homepage/SavingsCarousel.tsx | 39 +++++++++++++++++---- src/components/Homepage/TrustLogos.tsx | 15 ++++++-- src/intl/en/page-index.json | 18 +++++----- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/components/Homepage/FeatureCards.tsx b/src/components/Homepage/FeatureCards.tsx index 19a0ed70675..19f4fac7129 100644 --- a/src/components/Homepage/FeatureCards.tsx +++ b/src/components/Homepage/FeatureCards.tsx @@ -1,4 +1,4 @@ -import { getTranslations } from "next-intl/server" +import { getLocale, getTranslations } from "next-intl/server" import { ChevronNext } from "@/components/Chevron" import { Image } from "@/components/Image" @@ -6,6 +6,7 @@ import { ButtonLink } from "@/components/ui/buttons/Button" import { Section, SectionHeader } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import freeAccessImage from "@/public/images/homepage/features/free-access.png" import globalImage from "@/public/images/homepage/features/global.png" @@ -22,6 +23,12 @@ const FeatureCards = async ({ eventCategory = "Homepage", }: FeatureCardsProps) => { const t = await getTranslations("page-index") + const locale = await getLocale() + + const volume = numberFormat(locale, { + notation: "compact", + maximumSignificantDigits: 2, + }).format(4_600_000_000) return (

- {t("page-index-features-ownership-stat")} + {t("page-index-features-ownership-stat", { volume })}

{t("page-index-features-ownership-stat-label")} diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx index 0d2b6cbc49c..f6337c4a5c0 100644 --- a/src/components/Homepage/GetStartedGrid.tsx +++ b/src/components/Homepage/GetStartedGrid.tsx @@ -1,5 +1,5 @@ import { Book, Building2, ChevronRight, Code } from "lucide-react" -import { getTranslations } from "next-intl/server" +import { getLocale, getTranslations } from "next-intl/server" import { Image } from "@/components/Image" import { Card, CardContent } from "@/components/ui/card" @@ -7,6 +7,7 @@ import { LinkBox, LinkOverlay } from "@/components/ui/link-box" import { Section, SectionHeader } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import { ENTERPRISE_ETHEREUM_URL } from "@/lib/constants" @@ -24,6 +25,9 @@ const GetStartedGrid = async ({ eventCategory = "Homepage", }: GetStartedGridProps) => { const t = await getTranslations("page-index") + const locale = await getLocale() + + const minutes = numberFormat(locale).format(2) const cards = [ { @@ -87,7 +91,7 @@ const GetStartedGrid = async ({ {t("page-index-get-started-title")}

- {t("page-index-get-started-subtitle")} + {t("page-index-get-started-subtitle", { minutes })}

diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index 7ad42755cf5..9935d68018c 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { motion, useAnimationControls } from "motion/react" -import { useTranslations } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import type { Swiper as SwiperType } from "swiper" import { SwiperSlide } from "swiper/react" @@ -21,6 +21,7 @@ import { import { cn } from "@/lib/utils/cn" import { trackCustomEvent } from "@/lib/utils/matomo" +import { numberFormat } from "@/lib/utils/numbers" import FloatingCard from "./FloatingCard" @@ -54,6 +55,24 @@ type Slide = { function useSlides(): Slide[] { const t = useTranslations("page-index") + const locale = useLocale() + + const fmt = (value: number, options?: Intl.NumberFormatOptions) => + numberFormat(locale, options).format(value) + + const wireFee = fmt(50, { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }) + const txFee = fmt(0.02, { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + const twelve = fmt(12) + return [ { id: "privacy", @@ -80,20 +99,28 @@ function useSlides(): Slide[] { { id: "remittances", tag: t("page-index-carousel-remittances-tag"), - title: t("page-index-carousel-remittances-title"), - subtitle: t("page-index-carousel-remittances-subtitle"), - description: t("page-index-carousel-remittances-description"), + title: t("page-index-carousel-remittances-title", { minutes: twelve }), + subtitle: t("page-index-carousel-remittances-subtitle", { + wireFee, + days: fmt(5), + }), + description: t("page-index-carousel-remittances-description", { txFee }), cta: t("page-index-carousel-remittances-cta"), href: "/payments/", image: remittancesImage, comparison: { traditional: { label: t("page-index-carousel-remittances-traditional-label"), - value: t("page-index-carousel-remittances-traditional-value"), + value: t("page-index-carousel-remittances-traditional-value", { + min: fmt(3), + max: fmt(5), + }), }, ethereum: { label: t("page-index-carousel-remittances-ethereum-label"), - value: t("page-index-carousel-remittances-ethereum-value"), + value: t("page-index-carousel-remittances-ethereum-value", { + minutes: twelve, + }), }, }, }, diff --git a/src/components/Homepage/TrustLogos.tsx b/src/components/Homepage/TrustLogos.tsx index 322809e9e54..29c54a4f72b 100644 --- a/src/components/Homepage/TrustLogos.tsx +++ b/src/components/Homepage/TrustLogos.tsx @@ -1,5 +1,5 @@ import { ArrowRight, Check } from "lucide-react" -import { getTranslations } from "next-intl/server" +import { getLocale, getTranslations } from "next-intl/server" import { Image } from "@/components/Image" import { BaseLink } from "@/components/ui/Link" @@ -11,6 +11,7 @@ import { } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" +import { numberFormat } from "@/lib/utils/numbers" import FloatingCard from "./FloatingCard" @@ -26,6 +27,14 @@ const TrustLogos = async ({ eventCategory = "Homepage", }: TrustLogosProps) => { const t = await getTranslations("page-index") + const locale = await getLocale() + + const uptime = numberFormat(locale, { + style: "percent", + maximumFractionDigits: 0, + }).format(1) + const count = numberFormat(locale).format(10) + return (
- {t("page-index-trust-uptime")} + {t("page-index-trust-uptime", { uptime })}

- {t("page-index-trust-years")} + {t("page-index-trust-years", { count })}

{t("page-index-trust-since")} diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index c35466e1899..5fd09f60835 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -45,14 +45,14 @@ "page-index-carousel-privacy-ethereum-label": "ETHEREUM APPS", "page-index-carousel-privacy-ethereum-value": "Private by default", "page-index-carousel-remittances-tag": "CROSS-BORDER PAYMENTS", - "page-index-carousel-remittances-title": "Send money home in 12 minutes", - "page-index-carousel-remittances-subtitle": "Skip the $50 wire fee and the 5+ day wait.", - "page-index-carousel-remittances-description": "Send stablecoins to anyone, anywhere in the world, for just $0.02. They receive the funds almost instantly.", + "page-index-carousel-remittances-title": "Send money home in {minutes} minutes", + "page-index-carousel-remittances-subtitle": "Skip the {wireFee} wire fee and the {days}+ day wait.", + "page-index-carousel-remittances-description": "Send stablecoins to anyone, anywhere in the world, for just {txFee}. They receive the funds almost instantly.", "page-index-carousel-remittances-cta": "Try it yourself →", "page-index-carousel-remittances-traditional-label": "WIRE TRANSFER", - "page-index-carousel-remittances-traditional-value": "3-5 days", + "page-index-carousel-remittances-traditional-value": "{min}-{max} days", "page-index-carousel-remittances-ethereum-label": "ETHEREUM", - "page-index-carousel-remittances-ethereum-value": "12 minutes", + "page-index-carousel-remittances-ethereum-value": "{minutes} minutes", "page-index-carousel-borrowing-tag": "FINANCIAL ACCESS", "page-index-carousel-borrowing-title": "Borrow without credit history", "page-index-carousel-borrowing-subtitle": "You don't need a credit score to get started.", @@ -64,8 +64,8 @@ "page-index-carousel-borrowing-ethereum-value": "Based on collateral", "page-index-trust-image-alt": "Ethereum community illustration", "page-index-trust-never-offline": "Never offline", - "page-index-trust-uptime": "100% uptime", - "page-index-trust-years": "10 years", + "page-index-trust-uptime": "{uptime} uptime", + "page-index-trust-years": "{count} years", "page-index-trust-since": "Since 2015", "page-index-trust-tag": "Proven track record", "page-index-trust-title": "Built to last", @@ -82,7 +82,7 @@ "page-index-features-ownership-description-1": "Your bank balance is a ", "page-index-features-ownership-description-custody": "custody promise", "page-index-features-ownership-description-2": "Your Ethereum balance is true ownership.", - "page-index-features-ownership-stat": "4.6B+", + "page-index-features-ownership-stat": "{volume}+", "page-index-features-ownership-stat-label": "Daily trading volume", "page-index-features-public-rules-title": "Public rules", "page-index-features-public-rules-description": "The code is public, agreements execute exactly as written. Think vending machine versus hoping the cashier gives correct change.", @@ -94,7 +94,7 @@ "page-index-features-nobody-owns-description": "Changes happen through open proposals that anyone can participate in. Think community garden versus corporate farm.", "page-index-features-cta": "What is Ethereum?", "page-index-get-started-title": "Get started on Ethereum", - "page-index-get-started-subtitle": "Takes 2 minutes to get started. No credit check, no paperwork, no minimum balance.", + "page-index-get-started-subtitle": "Takes {minutes} minutes to get started. No credit check, no paperwork, no minimum balance.", "page-index-get-started-learn-title": "Understand Ethereum", "page-index-get-started-learn-description": "Start here. Learn what it is, why it matters, and how it works in plain language.", "page-index-get-started-learn-bullet-1": "What is Ethereum?", From ee909ce3843636822379eb2ffdd5241176c3f9f7 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 12:35:49 +0200 Subject: [PATCH 012/109] i18n: extract simulator hardcoded strings into new simulator namespace --- app/[locale]/page.tsx | 2 +- app/[locale]/wallets/page.tsx | 5 +- .../Homepage/SimulatorSection/index.tsx | 6 +- src/components/Simulator/Explanation.tsx | 4 +- src/components/Simulator/ProgressCta.tsx | 40 +- .../Simulator/WalletHome/AddressPill.tsx | 30 +- .../WalletHome/SendReceiveButton.tsx | 66 +- .../WalletHome/SendReceiveButtons.tsx | 6 +- .../Simulator/WalletHome/WalletBalance.tsx | 38 +- src/components/Simulator/WalletHome/index.tsx | 6 +- .../screens/SendReceive/ReceiveEther.tsx | 92 +- .../screens/SendReceive/ReceivedEther.tsx | 14 +- .../screens/SendReceive/SendEther.tsx | 21 +- .../screens/SendReceive/SendFromContacts.tsx | 18 +- .../screens/SendReceive/SendSummary.tsx | 13 +- .../Simulator/screens/SendReceive/Success.tsx | 15 +- src/data/WalletSimulatorData.tsx | 826 +++++++++--------- src/intl/en/simulator.json | 67 ++ 18 files changed, 688 insertions(+), 581 deletions(-) create mode 100644 src/intl/en/simulator.json diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index b60a7fb3a23..a3fdf93afeb 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -73,7 +73,7 @@ const Page = async (props: { params: Promise }) => { const allMessages = await getMessages() const glossary = allMessages["glossary-tooltip"] as Record const messages = { - ...pick(allMessages, "page-index"), + ...pick(allMessages, "page-index", "simulator"), "glossary-tooltip": pick(glossary, [ "nft-term", "nft-definition", diff --git a/app/[locale]/wallets/page.tsx b/app/[locale]/wallets/page.tsx index 22c478fdc9b..057932450a5 100644 --- a/app/[locale]/wallets/page.tsx +++ b/app/[locale]/wallets/page.tsx @@ -30,7 +30,7 @@ import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getMetadata } from "@/lib/utils/metadata" import { getRequiredNamespacesForPage } from "@/lib/utils/translations" -import { walletOnboardingSimData } from "@/data/WalletSimulatorData" +import { buildSimulatorData } from "@/data/WalletSimulatorData" import WalletsPageJsonLD from "./page-jsonld" @@ -50,6 +50,7 @@ const Page = async (props: { params: Promise }) => { const params = await props.params const { locale } = params const t = await getTranslations("page-wallets") + const simT = await getTranslations("simulator") setRequestLocale(locale) @@ -302,7 +303,7 @@ const Page = async (props: { params: Promise }) => { {locale === "en" ? (

- +

Interactive tutorial

diff --git a/src/components/Homepage/SimulatorSection/index.tsx b/src/components/Homepage/SimulatorSection/index.tsx index b548b10d22d..b6093f438aa 100644 --- a/src/components/Homepage/SimulatorSection/index.tsx +++ b/src/components/Homepage/SimulatorSection/index.tsx @@ -12,7 +12,7 @@ import { Section } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" -import { walletOnboardingSimData } from "@/data/WalletSimulatorData" +import { useWalletOnboardingSimData } from "@/data/WalletSimulatorData" type SimulatorSectionProps = { className?: string @@ -28,9 +28,9 @@ const SimulatorSkeleton = () => (
) -const sendReceiveData = walletOnboardingSimData[SEND_RECEIVE] - const SimulatorSection = ({ className, header }: SimulatorSectionProps) => { + const walletOnboardingSimData = useWalletOnboardingSimData() + const sendReceiveData = walletOnboardingSimData[SEND_RECEIVE] const { ref: sectionRef, isIntersecting: isVisible } = useIntersectionObserver({ rootMargin: "200px", diff --git a/src/components/Simulator/Explanation.tsx b/src/components/Simulator/Explanation.tsx index 078752cfc72..77af31164d9 100644 --- a/src/components/Simulator/Explanation.tsx +++ b/src/components/Simulator/Explanation.tsx @@ -1,6 +1,7 @@ import React from "react" import { ArrowLeft } from "lucide-react" import { motion } from "motion/react" +import { useTranslations } from "next-intl" import type { SimulatorNavProps } from "@/lib/types" @@ -35,6 +36,7 @@ export const Explanation = ({ openPath, logFinalCta, }: ExplanationProps) => { + const t = useTranslations("simulator") const { regressStepper, step, totalSteps } = nav const { header, description } = explanation @@ -62,7 +64,7 @@ export const Explanation = ({ animate={step === 0 ? "hidden" : "visible"} > - Back + {t("sim-back")} diff --git a/src/components/Simulator/ProgressCta.tsx b/src/components/Simulator/ProgressCta.tsx index 9b0cd5f418c..5280a4de4fe 100644 --- a/src/components/Simulator/ProgressCta.tsx +++ b/src/components/Simulator/ProgressCta.tsx @@ -1,5 +1,6 @@ import React, { type ComponentPropsWithoutRef } from "react" import { motion } from "motion/react" +import { useTranslations } from "next-intl" import { cn } from "@/lib/utils/cn" @@ -23,22 +24,25 @@ export const ProgressCta = ({ children, className, ...flexProps -}: ProgressCtaProps) => ( - - - -) + + + ) +} diff --git a/src/components/Simulator/WalletHome/AddressPill.tsx b/src/components/Simulator/WalletHome/AddressPill.tsx index 2e378672a5e..a86ecb56e44 100644 --- a/src/components/Simulator/WalletHome/AddressPill.tsx +++ b/src/components/Simulator/WalletHome/AddressPill.tsx @@ -1,4 +1,5 @@ import { Clipboard } from "lucide-react" +import { useTranslations } from "next-intl" import { Flex, type FlexProps } from "@/components/ui/flex" @@ -7,17 +8,20 @@ import { NotificationPopover } from "../NotificationPopover" type AddressPillProps = Omit -export const AddressPill = ({ ...btnProps }: AddressPillProps) => ( - - { + const t = useTranslations("simulator") + return ( + -

{FAKE_DEMO_ADDRESS}

- -
-
-) + +

{FAKE_DEMO_ADDRESS}

+ +
+ + ) +} diff --git a/src/components/Simulator/WalletHome/SendReceiveButton.tsx b/src/components/Simulator/WalletHome/SendReceiveButton.tsx index 60f6e284eb3..e051f005bbf 100644 --- a/src/components/Simulator/WalletHome/SendReceiveButton.tsx +++ b/src/components/Simulator/WalletHome/SendReceiveButton.tsx @@ -1,5 +1,6 @@ import { type ReactNode } from "react" import { LucideIcon } from "lucide-react" +import { useTranslations } from "next-intl" import { Button } from "@/components/ui/buttons/Button" @@ -24,39 +25,42 @@ export const SendReceiveButton = ({ isDisabled, onClick, isAnimated, -}: SendReceiveButtonProps) => ( - -) + {!isDisabled && isAnimated && } + + +
+

+ {children} +

+ {!isDisabled && isAnimated && ( + {t("sim-click")} + )} +
+ + ) +} diff --git a/src/components/Simulator/WalletHome/SendReceiveButtons.tsx b/src/components/Simulator/WalletHome/SendReceiveButtons.tsx index 9db04bd9506..a49aeffc8f5 100644 --- a/src/components/Simulator/WalletHome/SendReceiveButtons.tsx +++ b/src/components/Simulator/WalletHome/SendReceiveButtons.tsx @@ -1,4 +1,5 @@ import { QrCode, SendHorizontal } from "lucide-react" +import { useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" @@ -15,6 +16,7 @@ export const SendReceiveButtons = ({ nav, isEnabled = [false, false], }: SendReceiveButtonsProps) => { + const t = useTranslations("simulator") const [isSendEnabled, isReceiveEnabled] = isEnabled if (nav && isSendEnabled && isReceiveEnabled) throw new Error( @@ -32,7 +34,7 @@ export const SendReceiveButtons = ({ isHighlighted={highlightSend} icon={SendHorizontal} > - Send + {t("sim-send")} - Receive + {t("sim-receive")}
) diff --git a/src/components/Simulator/WalletHome/WalletBalance.tsx b/src/components/Simulator/WalletHome/WalletBalance.tsx index 30f499066dd..818803401e1 100644 --- a/src/components/Simulator/WalletHome/WalletBalance.tsx +++ b/src/components/Simulator/WalletHome/WalletBalance.tsx @@ -1,4 +1,5 @@ import React from "react" +import { useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" @@ -12,19 +13,24 @@ type WalletBalanceProps = { usdAmount?: number } -export const WalletBalance = ({ usdAmount = 0 }: WalletBalanceProps) => ( -
-

Your total

-

- {numberFormat("en-US", { - style: "currency", - currency: "USD", - notation: "compact", - maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), - }).format(usdAmount)} -

- - - -
-) +export const WalletBalance = ({ usdAmount = 0 }: WalletBalanceProps) => { + const t = useTranslations("simulator") + return ( +
+

+ {t("sim-your-total")} +

+

+ {numberFormat("en-US", { + style: "currency", + currency: "USD", + notation: "compact", + maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), + }).format(usdAmount)} +

+ + + +
+ ) +} diff --git a/src/components/Simulator/WalletHome/index.tsx b/src/components/Simulator/WalletHome/index.tsx index 1cd446c4b21..0f37232b73b 100644 --- a/src/components/Simulator/WalletHome/index.tsx +++ b/src/components/Simulator/WalletHome/index.tsx @@ -1,4 +1,5 @@ import { type Dispatch, type SetStateAction } from "react" +import { useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" @@ -6,7 +7,7 @@ import { defaultTokenBalances } from "../constants" import type { SimulatorNav } from "../interfaces" import { CategoryTabs } from "./CategoryTabs" -import { NFT, TokenBalance } from "./interfaces" +import type { NFT, TokenBalance } from "./interfaces" import { NFTList } from "./NFTList" import { SendReceiveButtons } from "./SendReceiveButtons" import { TokenBalanceList } from "./TokenBalanceList" @@ -30,6 +31,7 @@ export const WalletHome = ({ setActiveTabIndex, nfts = [], }: WalletHomeProps) => { + const t = useTranslations("simulator") const data: Array = tokenBalances ?? defaultTokenBalances const totalAmounts = data.reduce( (acc, { amount, usdConversion }) => acc + amount * usdConversion, @@ -43,7 +45,7 @@ export const WalletHome = ({ diff --git a/src/components/Simulator/screens/SendReceive/ReceiveEther.tsx b/src/components/Simulator/screens/SendReceive/ReceiveEther.tsx index 2e648d89cbd..1cba8bd70c8 100644 --- a/src/components/Simulator/screens/SendReceive/ReceiveEther.tsx +++ b/src/components/Simulator/screens/SendReceive/ReceiveEther.tsx @@ -1,4 +1,5 @@ import { motion } from "motion/react" +import { useTranslations } from "next-intl" import EthGlyph from "@/components/icons/eth-glyph-solid.svg" import { Image } from "@/components/Image" @@ -10,52 +11,55 @@ import { NotificationPopover } from "../../NotificationPopover" import QrImage from "@/public/images/qr-code-ethereum-org.png" -export const ReceiveEther = () => ( - -

Receive assets

-

- Show this QR code containing your account address to the sender -

- {/* QR Code */} - { + const t = useTranslations("simulator") + return ( + -
- -
- -
- - -
-

Your Ethereum address

-

{FAKE_DEMO_ADDRESS}

-
+

+ {t("sim-receive-title")} +

+

{t("sim-receive-qr-desc")}

+ {/* QR Code */} - +
+ +
+ +
- -

- Use this address for receiving tokens and NFTs on the Ethereum network. -

- -) + +
+

+ {t("sim-receive-your-address")} +

+

{FAKE_DEMO_ADDRESS}

+
+ + + +
+

{t("sim-receive-address-note")}

+ + ) +} diff --git a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx index 24dafb07d1b..b73cc53105e 100644 --- a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx +++ b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react" import { Info, X } from "lucide-react" import { AnimatePresence, motion } from "motion/react" +import { useTranslations } from "next-intl" import type { SimulatorNavProps } from "@/lib/types" @@ -23,6 +24,7 @@ export const ReceivedEther = ({ ethPrice, sender, }: ReceivedEtherProps) => { + const t = useTranslations("simulator") const [received, setReceived] = useState(false) const [hideToast, setHideToast] = useState(false) const showToast = received && !hideToast @@ -96,8 +98,16 @@ export const ReceivedEther = ({ >

- You received {displayEth} ETH ({displayUsd}) - {sender ? ` from ${sender}` : ""}! + {sender + ? t("sim-received-toast", { + ethAmount: displayEth, + usdAmount: displayUsd, + sender, + }) + : t("sim-received-toast-no-sender", { + ethAmount: displayEth, + usdAmount: displayUsd, + })}

setHidden(true)} /> diff --git a/src/components/Simulator/screens/SendReceive/SendEther.tsx b/src/components/Simulator/screens/SendReceive/SendEther.tsx index cd87450f7df..c7ffea4e155 100644 --- a/src/components/Simulator/screens/SendReceive/SendEther.tsx +++ b/src/components/Simulator/screens/SendReceive/SendEther.tsx @@ -1,4 +1,5 @@ import React from "react" +import { useTranslations } from "next-intl" import { Button } from "@/components/ui/buttons/Button" import { Flex, HStack } from "@/components/ui/flex" @@ -21,6 +22,8 @@ export const SendEther = ({ chosenAmount, setChosenAmount, }: SendEtherProps) => { + const t = useTranslations("simulator") + const formatDollars = (amount: number): string => numberFormat("en-US", { style: "currency", @@ -59,14 +62,16 @@ export const SendEther = ({ return (
-

Send

-

How much do you want to send?

+

+ {t("sim-send-title")} +

+

{t("sim-send-how-much")}

{/* Left side: Displayed send amount */} {/* Token selector pill */} @@ -95,7 +100,9 @@ export const SendEther = ({ {/* Balances */} -

Balance: {usdAmount}

+

+ {t("sim-send-balance", { amount: usdAmount })} +

<>{ethAmount} ETH

diff --git a/src/components/Simulator/screens/SendReceive/SendFromContacts.tsx b/src/components/Simulator/screens/SendReceive/SendFromContacts.tsx index 7574206faec..c2bdde8566d 100644 --- a/src/components/Simulator/screens/SendReceive/SendFromContacts.tsx +++ b/src/components/Simulator/screens/SendReceive/SendFromContacts.tsx @@ -1,4 +1,5 @@ import { QrCode, Search } from "lucide-react" +import { useTranslations } from "next-intl" import type { SimulatorNavProps } from "@/lib/types" @@ -18,6 +19,7 @@ export const SendFromContacts = ({ nav, setRecipient, }: SendFromContactsProps) => { + const t = useTranslations("simulator") const handleSelection = (name: string) => { setRecipient(name) nav.progressStepper() @@ -25,29 +27,31 @@ export const SendFromContacts = ({ return ( <>
-

Choose recipient

+

+ {t("sim-contacts-title")} +

- {CONTACTS.map(({ name, lastAction }, i) => ( + {CONTACTS.map(({ name }, i) => ( diff --git a/src/components/Simulator/screens/SendReceive/SendSummary.tsx b/src/components/Simulator/screens/SendReceive/SendSummary.tsx index 8a541cfb828..5ec44b890d4 100644 --- a/src/components/Simulator/screens/SendReceive/SendSummary.tsx +++ b/src/components/Simulator/screens/SendReceive/SendSummary.tsx @@ -1,4 +1,5 @@ import React from "react" +import { useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" @@ -19,6 +20,8 @@ export const SendSummary = ({ ethPrice, recipient, }: SendSummaryProps) => { + const t = useTranslations("simulator") + const formatEth = (amount: number): string => numberFormat("en", { maximumFractionDigits: 5 }).format(amount) @@ -35,7 +38,7 @@ export const SendSummary = ({ {/* Top section */}

- You are sending + {t("sim-summary-title")}

-

To

+

{t("sim-summary-to")}

{recipient}

-

Arrival time

-

est. about 12 seconds

+

{t("sim-summary-arrival")}

+

{t("sim-summary-arrival-est")}

-

Network fees

+

{t("sim-summary-fees")}

{numberFormat("en", { maximumFractionDigits: getMaxFractionDigitsUsd(usdFee), diff --git a/src/components/Simulator/screens/SendReceive/Success.tsx b/src/components/Simulator/screens/SendReceive/Success.tsx index 495d2384a9c..b7537f3a547 100644 --- a/src/components/Simulator/screens/SendReceive/Success.tsx +++ b/src/components/Simulator/screens/SendReceive/Success.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react" import { Check } from "lucide-react" import { AnimatePresence, motion } from "motion/react" +import { useTranslations } from "next-intl" import { Flex, VStack } from "@/components/ui/flex" import { Spinner } from "@/components/ui/spinner" @@ -26,6 +27,7 @@ export const Success = ({ ethPrice, recipient, }: SuccessProps) => { + const t = useTranslations("simulator") const [txPending, setTxPending] = useState(true) const [showWallet, setShowWallet] = useState(false) const [categoryIndex, setCategoryIndex] = useState(0) @@ -113,14 +115,15 @@ export const Success = ({ )}

{txPending ? ( - "Sending transaction" + t("sim-success-sending") ) : ( - You sent{" "} - - <>{sentEthValue} ETH - {" "} - ({usdValue}) to {recipient} + {t.rich("sim-success-sent", { + ethAmount: sentEthValue, + usdAmount: usdValue, + recipient, + strong: (chunks) => {chunks}, + })} )}

diff --git a/src/data/WalletSimulatorData.tsx b/src/data/WalletSimulatorData.tsx index e8be24148b5..384808b1579 100644 --- a/src/data/WalletSimulatorData.tsx +++ b/src/data/WalletSimulatorData.tsx @@ -1,5 +1,7 @@ "use client" +import { useTranslations } from "next-intl" + import { Stack } from "@/components/ui/flex" import Link from "@/components/ui/Link" import { ListItem, OrderedList, UnorderedList } from "@/components/ui/list" @@ -24,431 +26,413 @@ import { import { CONTACTS } from "../components/Simulator/screens/SendReceive/constants" import type { SimulatorData } from "../components/Simulator/types" -export const walletOnboardingSimData: SimulatorData = { - [CREATE_ACCOUNT]: { - title: "Create account", - Icon: CreateAccountIcon, - Screen: CreateAccount, - explanations: [ - { - header: "Begin your journey by downloading a wallet", - description: ( - <> -

To get started, you'll need to download a wallet app.

-

- Most people use mobile apps, but desktop apps and browser - extensions are also available. -

-

- Let's set up a mobile wallet. Click "Install a - wallet" to get started. -

- - ), - }, - { - header: "Wallets are free apps you can download", - description: ( - <> -

- Mobile wallet apps can be downloaded and installed using any app - store. -

-

- Wallets provide an easy way to create an Ethereum account, and - then use Ethereum and its applications. -

-

Go ahead and open your new wallet app.

- - ), - }, - { - header: "Creating an account is free, private and easy", - description: ( - <> -

- Ethereum accounts are created privately and do not require any - forms or approval—no personal identifying information required! -

-

- Click on "Create account" to generate a new account. -

- - ), - }, - { - header: - "This is YOUR account, and nobody else's—you control it completely", - description: ( -

- No company, including your wallet provider, has access to your - account. -

- ), - }, - { - header: "A recovery phrase is used to keep the account safe", - description: ( - <> -

- You and only you control this phrase, so it is critical to take - steps to backup and secure it. -

-

- Read carefully and click "I understand" to see and - backup your recovery phrase. -

- - ), - }, - { - header: "Keep your phrase safe!", - description: ( - <> - -

Storing small amount of value:

- - - Consider saving in a - password manager - - -
- -

Storing any significant value:

- - - Write your recovery - phrase down - - - Store it in a safe place - (consider multiple backups) - - {/* TODO: Add link for seed phrase further reading */} - {/* - {" "} - - Learn more on protecting your recovery phrase - - */} - -
- -

Unsafe backup methods:

- - - - Texting it to a friend (or anyone!) - - - - Taking a picture of the phrase - - - - Saving it in a file on your computer - - -
- - ), - }, - { - header: "Repeat phrase to prove you have saved it", - description: ( - <> -

- This is done on initial setup only, but is not{" "} - required every time. -

-

- Keep this private! Nobody from customer service - should ever ask you for this. -

-

- Click the words in the correct order to prove you've backed - up your phrase. -

- - ), - }, - { - header: "That's it! Welcome to Ethereum 🎉", - description: ( -

- In the next lesson we'll learn how to use your new account to - receive and send some funds. -

- ), +/** + * The translate function shape shared by both useTranslations (client) + * and getTranslations (server) from next-intl. + */ +type TranslateFn = ReturnType> + +/** + * Hook returning translated simulator data for client components. + */ +export function useWalletOnboardingSimData(): SimulatorData { + const t = useTranslations("simulator") + return buildSimulatorData(t) +} + +/** + * Build simulator data with the provided translate function. + * Works with both useTranslations (client) and getTranslations (server). + */ +export function buildSimulatorData(t: TranslateFn): SimulatorData { + return { + [CREATE_ACCOUNT]: { + title: "Create account", + Icon: CreateAccountIcon, + Screen: CreateAccount, + explanations: [ + { + header: "Begin your journey by downloading a wallet", + description: ( + <> +

To get started, you'll need to download a wallet app.

+

+ Most people use mobile apps, but desktop apps and browser + extensions are also available. +

+

+ Let's set up a mobile wallet. Click "Install a + wallet" to get started. +

+ + ), + }, + { + header: "Wallets are free apps you can download", + description: ( + <> +

+ Mobile wallet apps can be downloaded and installed using any app + store. +

+

+ Wallets provide an easy way to create an Ethereum account, and + then use Ethereum and its applications. +

+

Go ahead and open your new wallet app.

+ + ), + }, + { + header: "Creating an account is free, private and easy", + description: ( + <> +

+ Ethereum accounts are created privately and do not require any + forms or approval—no personal identifying information required! +

+

+ Click on "Create account" to generate a new account. +

+ + ), + }, + { + header: + "This is YOUR account, and nobody else's—you control it completely", + description: ( +

+ No company, including your wallet provider, has access to your + account. +

+ ), + }, + { + header: "A recovery phrase is used to keep the account safe", + description: ( + <> +

+ You and only you control this phrase, so it is critical to take + steps to backup and secure it. +

+

+ Read carefully and click "I understand" to see and + backup your recovery phrase. +

+ + ), + }, + { + header: "Keep your phrase safe!", + description: ( + <> + +

Storing small amount of value:

+ + + Consider saving in a + password manager + + +
+ +

Storing any significant value:

+ + + Write your recovery + phrase down + + + Store it in a safe + place (consider multiple backups) + + +
+ +

Unsafe backup methods:

+ + + + Texting it to a friend (or anyone!) + + + + Taking a picture of the phrase + + + + Saving it in a file on your computer + + +
+ + ), + }, + { + header: "Repeat phrase to prove you have saved it", + description: ( + <> +

+ This is done on initial setup only, but is not{" "} + required every time. +

+

+ Keep this private! Nobody from customer service + should ever ask you for this. +

+

+ Click the words in the correct order to prove you've backed + up your phrase. +

+ + ), + }, + { + header: "That's it! Welcome to Ethereum 🎉", + description: ( +

+ In the next lesson we'll learn how to use your new account to + receive and send some funds. +

+ ), + }, + ], + ctaLabels: [ + "Install a wallet", + "Open wallet", + "Create account", + "Next", + "I understand", + "Next", + "Start using wallet", + ], + finalCtaLink: { + label: "Download a real wallet", + href: "/wallets/find-wallet/", }, - ], - ctaLabels: [ - "Install a wallet", - "Open wallet", - "Create account", - "Next", - "I understand", - "Next", - "Start using wallet", - ], - finalCtaLink: { - label: "Download a real wallet", - href: "/wallets/find-wallet/", + nextPathId: SEND_RECEIVE, }, - nextPathId: SEND_RECEIVE, - }, - [SEND_RECEIVE]: { - title: "Send / receive tokens", - Icon: SendReceiveIcon, - Screen: SendReceive, - explanations: [ - { - header: "Receive digital assets from anywhere", - description: ( - <> -

- Your wallet helps you manage your funds,{" "} - NFTs,{" "} - Web3 identity - and more. Here we'll go over how to receive and send some - tokens on Ethereum. -

-

- Let's first look at how to receive ether (ETH), - Ethereum's native currency. -

-

- Click the "Receive" button to see how to receive funds. -

- - ), - }, - { - header: "Receiving tokens is as easy as sharing your address", - description: ( - <> -

- Your address is a sharable identifier for your - account—share this with others to receive tokens. -

-

- An Ethereum address is like a transparent public dropbox, with - your own unique number on it—anyone can see in, or put stuff - inside, but only you have the ability to unlock and use its - contents. -

- - ), - }, - { - header: "You received ether (ETH)! Now let's send some", - description: ( - <> -

- Now you have some ETH to cover network fees, allowing you to - submit transactions yourself. -

-

- Note that you didn't need to provide any personal - information, or have any funds to begin with to start receiving - assets to your address—receiving is free. -

-

- Let's try sending some ETH by clicking the "Send" - button. -

- - ), - }, - { - header: "Sending tokens is quick and irreversible", - description: ( - <> -

- Unlike with traditional banking, there are no borders, or third - parties intervening and stopping your transactions. -

-

- Ethereum doesn't discriminate, and never stops, allowing you - full control over your funds—24/7. -

-

- Select an amount to send then click "Select recipient." -

- - ), - }, - { - header: "You can save contacts to make it easier", - description: ( - <> -

- To send tokens, you only need to know the recipients Ethereum - address. -

-

You can send tokens anywhere globally at any time.

-

- As you use your wallet, you can save users as contacts for - repeated use. Let's send some funds back to{" "} - {CONTACTS[0].name}. -

- - ), - }, - { - header: "You will need small amount of ETH to send tokens (fee)", - description: ( - <> -

- Make sure your account has enough ETH to cover network fees. Fees - change based on how many people are using Ethereum. -

-

- Most wallets will automatically add the suggested fee to the - transaction which you can then confirm. -

- - ), + [SEND_RECEIVE]: { + title: t("sim-sr-title"), + Icon: SendReceiveIcon, + Screen: SendReceive, + explanations: [ + { + header: t("sim-sr-header-1"), + description: ( + <> +

+ {t.rich("sim-sr-desc-1-p1", { + nft: (chunks) => ( + {chunks} + ), + web3: (chunks) => ( + {chunks} + ), + })} +

+

{t("sim-sr-desc-1-p2")}

+

{t("sim-sr-desc-1-p3")}

+ + ), + }, + { + header: t("sim-sr-header-2"), + description: ( + <> +

+ {t.rich("sim-sr-desc-2-p1", { + em: (chunks) => {chunks}, + })} +

+

{t("sim-sr-desc-2-p2")}

+ + ), + }, + { + header: t("sim-sr-header-3"), + description: ( + <> +

{t("sim-sr-desc-3-p1")}

+

{t("sim-sr-desc-3-p2")}

+

{t("sim-sr-desc-3-p3")}

+ + ), + }, + { + header: t("sim-sr-header-4"), + description: ( + <> +

{t("sim-sr-desc-4-p1")}

+

{t("sim-sr-desc-4-p2")}

+

{t("sim-sr-desc-4-p3")}

+ + ), + }, + { + header: t("sim-sr-header-5"), + description: ( + <> +

{t("sim-sr-desc-5-p1")}

+

{t("sim-sr-desc-5-p2")}

+

{t("sim-sr-desc-5-p3", { contactName: CONTACTS[0].name })}

+ + ), + }, + { + header: t("sim-sr-header-6"), + description: ( + <> +

{t("sim-sr-desc-6-p1")}

+

{t("sim-sr-desc-6-p2")}

+ + ), + }, + { + header: t("sim-sr-header-7"), + description:

{t("sim-sr-desc-7-p1")}

, + }, + ], + ctaLabels: [ + "", + t("sim-sr-cta-2"), + "", + t("sim-sr-cta-4"), + "", + t("sim-sr-cta-6"), + ], + finalCtaLink: { + label: t("sim-sr-final-cta"), + href: "/wallets/find-wallet/", }, - { - header: "Peer-to-peer. Global. Always available. 🎉", - description: ( -

- Start the next lesson to learn how to use your wallet to log into - Web3 applications. -

- ), - }, - ], - ctaLabels: ["", "Share address", "", "Select recipient", "", "Send now"], - finalCtaLink: { - label: "Download a real wallet", - href: "/wallets/find-wallet/", + nextPathId: CONNECT_WEB3, }, - nextPathId: CONNECT_WEB3, - }, - [CONNECT_WEB3]: { - title: "Connect to Web3", - Icon: ConnectWeb3Icon, - Screen: ConnectWeb3, - explanations: [ - { - header: "Explore Web3: from NFTs to decentralized finance and identity", - description: ( - <> -

- Your wallet can be used to connect to all sorts of applications, - allowing you to interact with your onchain assets. -

-

- Your friend just sent an NFT art piece to your address! Let's - go to a new NFT marketplace website to view it. -

- - ), - }, - { - header: "No need to create a new account for each service", - description: ( - <> -

- Your account is universal across all Ethereum and - Ethereum-compatible applications. -

-

Assets stored onchain can be accessed from any application.

- - ), - }, - { - header: "You can have a single login for most Ethereum based projects", - description: ( - <> -

- The same account address will represent your identity on many - different Ethereum compatible blockchains such as Arbitrum, - Polygon or Optimism. -

-

- Logins are handled by your wallet—no more creating insecure - passwords. -

- - ), - }, - { - header: "Personal identifying information is not shared", - description: ( - <> -

Your private information stays private.

-

- Your personal information, such as email or phone number, is not - needed to use Web3 apps—you only need a wallet. -

-

- Also note there are no associated transaction fees here—signing in - using Ethereum is free, fast and easy! -

- - ), - }, - { - header: - "No geographical or political discrimination against who can use Ethereum services", - description: ( - <> -

There's the NFT you received!

-

- Wallets are technically only an interface to show you your balance - and to make transactions— - - your assets aren't stored inside the wallet, but on the - blockchain. - -

- - ), - }, - { - header: "Start your journey now", - description: ( - <> -

Great job! You're ready to start using apps on Ethereum.

- -

What to do next:

- - - - Learn about staying safe in Web3 - - - - - Learn more about Ethereum - - - - - Check out some beginner friendly apps - - - -
- - ), + [CONNECT_WEB3]: { + title: "Connect to Web3", + Icon: ConnectWeb3Icon, + Screen: ConnectWeb3, + explanations: [ + { + header: + "Explore Web3: from NFTs to decentralized finance and identity", + description: ( + <> +

+ Your wallet can be used to connect to all sorts of applications, + allowing you to interact with your onchain assets. +

+

+ Your friend just sent an NFT art piece to your address! + Let's go to a new NFT marketplace website to view it. +

+ + ), + }, + { + header: "No need to create a new account for each service", + description: ( + <> +

+ Your account is universal across all Ethereum and + Ethereum-compatible applications. +

+

Assets stored onchain can be accessed from any application.

+ + ), + }, + { + header: + "You can have a single login for most Ethereum based projects", + description: ( + <> +

+ The same account address will represent your identity on many + different Ethereum compatible blockchains such as Arbitrum, + Polygon or Optimism. +

+

+ Logins are handled by your wallet—no more creating insecure + passwords. +

+ + ), + }, + { + header: "Personal identifying information is not shared", + description: ( + <> +

Your private information stays private.

+

+ Your personal information, such as email or phone number, is not + needed to use Web3 apps—you only need a wallet. +

+

+ Also note there are no associated transaction fees here—signing + in using Ethereum is free, fast and easy! +

+ + ), + }, + { + header: + "No geographical or political discrimination against who can use Ethereum services", + description: ( + <> +

There's the NFT you received!

+

+ Wallets are technically only an interface to show you your + balance and to make transactions— + + your assets aren't stored inside the wallet, but on the + blockchain. + +

+ + ), + }, + { + header: "Start your journey now", + description: ( + <> +

+ Great job! You're ready to start using apps on Ethereum. +

+ +

What to do next:

+ + + + Learn about staying safe in Web3 + + + + + Learn more about Ethereum + + + + + Check out some beginner friendly apps + + + +
+ + ), + }, + ], + ctaLabels: [ + "Visit NFT market", + "Connect wallet", + "Connect to app", + "Go to account", + "Finished", + ], + finalCtaLink: { + label: "Get a wallet", + href: "/wallets/find-wallet/", + isPrimary: true, }, - ], - ctaLabels: [ - "Visit NFT market", - "Connect wallet", - "Connect to app", - "Go to account", - "Finished", - ], - finalCtaLink: { - label: "Get a wallet", - href: "/wallets/find-wallet/", - isPrimary: true, }, - }, + } } diff --git a/src/intl/en/simulator.json b/src/intl/en/simulator.json new file mode 100644 index 00000000000..cb711042dc1 --- /dev/null +++ b/src/intl/en/simulator.json @@ -0,0 +1,67 @@ +{ + "sim-back": "Back", + "sim-your-total": "Your total", + "sim-send": "Send", + "sim-receive": "Receive", + "sim-crypto": "Crypto", + "sim-nfts": "NFTs", + "sim-click": "click!", + "sim-no-nfts": "No NFTs yet!", + "sim-example-walkthrough": "Example walkthrough", + "sim-address-tooltip": "Share your address (public identifier) from your own wallet when finished here", + "sim-contact-last-action": "Received 10 minutes ago", + "sim-sr-title": "Send / receive tokens", + "sim-sr-header-1": "Receive digital assets from anywhere", + "sim-sr-desc-1-p1": "Your wallet helps you manage your funds, NFTs, Web3 identity and more. Here we'll go over how to receive and send some tokens on Ethereum.", + "sim-sr-desc-1-p2": "Let's first look at how to receive ether (ETH), Ethereum's native currency.", + "sim-sr-desc-1-p3": "Click the \"Receive\" button to see how to receive funds.", + "sim-sr-header-2": "Receiving tokens is as easy as sharing your address", + "sim-sr-desc-2-p1": "Your address is a sharable identifier for your account—share this with others to receive tokens.", + "sim-sr-desc-2-p2": "An Ethereum address is like a transparent public dropbox, with your own unique number on it—anyone can see in, or put stuff inside, but only you have the ability to unlock and use its contents.", + "sim-sr-header-3": "You received ether (ETH)! Now let's send some", + "sim-sr-desc-3-p1": "Now you have some ETH to cover network fees, allowing you to submit transactions yourself.", + "sim-sr-desc-3-p2": "Note that you didn't need to provide any personal information, or have any funds to begin with to start receiving assets to your address—receiving is free. 😁", + "sim-sr-desc-3-p3": "Let's try sending some ETH by clicking the \"Send\" button.", + "sim-sr-header-4": "Sending tokens is quick and irreversible", + "sim-sr-desc-4-p1": "Unlike with traditional banking, there are no borders, or third parties intervening and stopping your transactions.", + "sim-sr-desc-4-p2": "Ethereum doesn't discriminate, and never stops, allowing you full control over your funds—24/7.", + "sim-sr-desc-4-p3": "Select an amount to send then click \"Select recipient.\"", + "sim-sr-header-5": "You can save contacts to make it easier", + "sim-sr-desc-5-p1": "To send tokens, you only need to know the recipients Ethereum address.", + "sim-sr-desc-5-p2": "You can send tokens anywhere globally at any time.", + "sim-sr-desc-5-p3": "As you use your wallet, you can save users as contacts for repeated use. Let's send some funds back to {contactName}.", + "sim-sr-header-6": "You will need small amount of ETH to send tokens (fee)", + "sim-sr-desc-6-p1": "Make sure your account has enough ETH to cover network fees. Fees change based on how many people are using Ethereum.", + "sim-sr-desc-6-p2": "Most wallets will automatically add the suggested fee to the transaction which you can then confirm.", + "sim-sr-header-7": "Peer-to-peer. Global. Always available. 🎉", + "sim-sr-desc-7-p1": "Start the next lesson to learn how to use your wallet to log into Web3 applications.", + "sim-sr-cta-2": "Share address", + "sim-sr-cta-4": "Select recipient", + "sim-sr-cta-6": "Send now", + "sim-sr-final-cta": "Download a real wallet", + "sim-receive-title": "Receive assets", + "sim-receive-qr-desc": "Show this QR code containing your account address to the sender", + "sim-receive-qr-share": "Share QR containing your address (public identifier) from your own wallet when finished here", + "sim-receive-your-address": "Your Ethereum address", + "sim-receive-copy": "Copy", + "sim-receive-address-note": "Use this address for receiving tokens and NFTs on the Ethereum network.", + "sim-received-toast": "You received {ethAmount} ETH ({usdAmount}) from {sender}!", + "sim-received-toast-no-sender": "You received {ethAmount} ETH ({usdAmount})!", + "sim-send-title": "Send", + "sim-send-how-much": "How much do you want to send?", + "sim-send-choose-value": "Choose a value below", + "sim-send-eth-only-note": "In this walkthrough you can only send ETH, but in real wallet you can send different tokens as well", + "sim-send-balance": "Balance: {amount}", + "sim-contacts-title": "Choose recipient", + "sim-contacts-choose": "Choose {name} from recent contacts", + "sim-contacts-search": "Address or contacts", + "sim-contacts-my-contacts": "My contacts", + "sim-contacts-recent": "Recent", + "sim-summary-title": "You are sending", + "sim-summary-to": "To", + "sim-summary-arrival": "Arrival time", + "sim-summary-arrival-est": "est. about 12 seconds", + "sim-summary-fees": "Network fees", + "sim-success-sending": "Sending transaction", + "sim-success-sent": "You sent {ethAmount} ETH ({usdAmount}) to {recipient}" +} From 823b13ed7b80ed0eb34c50dcd36550ca35de0140 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 12:51:45 +0200 Subject: [PATCH 013/109] i18n: replace hardcoded numbers and currency with locale-aware numberformat --- src/components/Homepage/KPISection.tsx | 24 +- .../Simulator/WalletHome/NFTList.tsx | 5 +- .../Simulator/screens/ConnectWeb3/Browser.tsx | 8 +- .../Simulator/screens/ConnectWeb3/Slider.tsx | 6 +- .../Simulator/screens/ConnectWeb3/Web3App.tsx | 6 +- .../Simulator/screens/ConnectWeb3/index.tsx | 45 ++-- .../screens/CreateAccount/GeneratingKeys.tsx | 6 +- .../CreateAccount/InitialWordDisplay.tsx | 24 +- .../CreateAccount/InteractiveWordSelector.tsx | 4 +- .../CreateAccount/RecoveryPhraseNotice.tsx | 40 +-- .../screens/CreateAccount/WelcomeScreen.tsx | 33 ++- src/data/WalletSimulatorData.tsx | 230 ++++++---------- src/intl/en/simulator.json | 99 ++++++- src/intl/es/page-index.json | 245 ++++++++---------- 14 files changed, 408 insertions(+), 367 deletions(-) diff --git a/src/components/Homepage/KPISection.tsx b/src/components/Homepage/KPISection.tsx index 5e9f02d6cec..fa36c8e1cfe 100644 --- a/src/components/Homepage/KPISection.tsx +++ b/src/components/Homepage/KPISection.tsx @@ -124,24 +124,16 @@ function AnimatedNumber({ return

{formatter(displayValue)}

} -/** - * Format large numbers with M/B suffix - */ -function formatNumber(value: number, locale: string): string { - if (value >= 1_000_000_000) { - return `${(value / 1_000_000_000).toFixed(1)}B` - } - if (value >= 1_000_000) { - return `${Math.round(value / 1_000_000)}M` - } - if (value >= 1_000) { - return numberFormat(locale).format(value) - } - return value.toString() +function formatCompact(value: number, locale: string): string { + return numberFormat(locale, { + notation: "compact", + maximumSignificantDigits: 3, + }).format(value) } /** - * Format transaction count with spaces (European style: 21 400 433) + * Format transaction count with spaces as thousands separator (design choice + * to avoid commas/dots that interfere with the animated counter). */ function formatTransactions(value: number): string { return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ") @@ -196,7 +188,7 @@ const KPISection = ({

{accountHolders !== null - ? formatNumber(accountHolders, locale) + ? formatCompact(accountHolders, locale) : "—"}

diff --git a/src/components/Simulator/WalletHome/NFTList.tsx b/src/components/Simulator/WalletHome/NFTList.tsx index 153bc19e6af..686211075bd 100644 --- a/src/components/Simulator/WalletHome/NFTList.tsx +++ b/src/components/Simulator/WalletHome/NFTList.tsx @@ -1,3 +1,5 @@ +import { useTranslations } from "next-intl" + import { Image } from "@/components/Image" import { Flex, type FlexProps } from "@/components/ui/flex" @@ -11,6 +13,7 @@ type NFTListProps = FlexProps & { nfts: Array } export const NFTList = ({ nfts, ...flexProps }: NFTListProps) => { + const t = useTranslations("simulator") const size = useBreakpointValue({ base: "max-w-20 max-h-20", md: "max-w-24 max-h-24", @@ -25,7 +28,7 @@ export const NFTList = ({ nfts, ...flexProps }: NFTListProps) => {

)) ) : ( -

No NFTs yet!

+

{t("sim-no-nfts")}

)} ) diff --git a/src/components/Simulator/screens/ConnectWeb3/Browser.tsx b/src/components/Simulator/screens/ConnectWeb3/Browser.tsx index 406ab9140d2..0996a2d4047 100644 --- a/src/components/Simulator/screens/ConnectWeb3/Browser.tsx +++ b/src/components/Simulator/screens/ConnectWeb3/Browser.tsx @@ -1,6 +1,7 @@ import React, { type HTMLAttributes, useEffect, useState } from "react" import { MoreHorizontal, Search, Triangle } from "lucide-react" import { motion } from "motion/react" +import { useTranslations } from "next-intl" import { Flex, HStack } from "@/components/ui/flex" @@ -13,6 +14,7 @@ import { EXAMPLE_APP_URL } from "./constants" type BrowserProps = HTMLAttributes export const Browser = ({ ...props }: BrowserProps) => { + const t = useTranslations("simulator") const [typing, setTyping] = useState(false) useEffect(() => { const timeout = setTimeout(() => { @@ -37,8 +39,8 @@ export const Browser = ({ ...props }: BrowserProps) => {
@@ -56,7 +58,7 @@ export const Browser = ({ ...props }: BrowserProps) => { ))} ) : ( -

Search or enter website

+

{t("sim-cw-search-website")}

)}
diff --git a/src/components/Simulator/screens/ConnectWeb3/Slider.tsx b/src/components/Simulator/screens/ConnectWeb3/Slider.tsx index f63e6eb8811..0d0297b0505 100644 --- a/src/components/Simulator/screens/ConnectWeb3/Slider.tsx +++ b/src/components/Simulator/screens/ConnectWeb3/Slider.tsx @@ -1,6 +1,7 @@ import { type ReactNode } from "react" import { Check } from "lucide-react" import { motion } from "motion/react" +import { useTranslations } from "next-intl" import { HStack, VStack } from "@/components/ui/flex" @@ -12,6 +13,7 @@ type SliderProps = { children: ReactNode } export const Slider = ({ isConnected, displayUrl, children }: SliderProps) => { + const t = useTranslations("simulator") return ( <> { transition={{ delay: 0.15 }} >

- You're logged in! + {t("sim-cw-logged-in")}

) : ( <>

- Connect account? + {t("sim-cw-connect-account")}

{/* URL Pill */} diff --git a/src/components/Simulator/screens/ConnectWeb3/Web3App.tsx b/src/components/Simulator/screens/ConnectWeb3/Web3App.tsx index 4e2425cdd44..f10aadbc091 100644 --- a/src/components/Simulator/screens/ConnectWeb3/Web3App.tsx +++ b/src/components/Simulator/screens/ConnectWeb3/Web3App.tsx @@ -1,5 +1,6 @@ import React, { type HTMLAttributes } from "react" import { Menu } from "lucide-react" +import { useTranslations } from "next-intl" import { HStack } from "@/components/ui/flex" @@ -20,6 +21,7 @@ export const Web3App = ({ className, ...rest }: Web3AppProps) => { + const t = useTranslations("simulator") return (
{displayUrl}

{/* TODO: Remove 'size' class when icon is migrated */} diff --git a/src/components/Simulator/screens/ConnectWeb3/index.tsx b/src/components/Simulator/screens/ConnectWeb3/index.tsx index 46ca2dc5c44..194786ae50a 100644 --- a/src/components/Simulator/screens/ConnectWeb3/index.tsx +++ b/src/components/Simulator/screens/ConnectWeb3/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react" import { AnimatePresence, motion } from "motion/react" +import { useTranslations } from "next-intl" import type { PhoneScreenProps } from "@/lib/types" @@ -32,10 +33,11 @@ import { Web3App } from "./Web3App" import NFTImage from "@/public/images/deep-panic.png" export const ConnectWeb3 = ({ nav, ctaLabel }: PhoneScreenProps) => { + const t = useTranslations("simulator") const { progressStepper, step } = nav const NFTs = [ { - title: "Cool art", + title: t("sim-cw-nft-title"), image: NFTImage, }, ] @@ -57,7 +59,7 @@ export const ConnectWeb3 = ({ nav, ctaLabel }: PhoneScreenProps) => { const [activeTabIndex, setActiveTabIndex] = useState(1) const nfts = [ { - title: "Cool art", + title: t("sim-cw-nft-title"), image: NFTImage, }, ] @@ -86,11 +88,13 @@ export const ConnectWeb3 = ({ nav, ctaLabel }: PhoneScreenProps) => { className="text-xl leading-[1.4] duration-700 md:text-2xl" {...fadeInProps} > - Welcome to Web3 - NFT Marketplace + {t("sim-cw-welcome-web3")} + + {t("sim-cw-nft-marketplace")} + - Connect your wallet to view your collection + {t("sim-cw-connect-wallet-prompt")} @@ -98,8 +102,7 @@ export const ConnectWeb3 = ({ nav, ctaLabel }: PhoneScreenProps) => { {[2, 3].includes(step) && ( - Connecting to the website will not share any personal or secure - information with the site owners. + {t("sim-cw-connect-disclaimer")} )} @@ -111,11 +114,13 @@ export const ConnectWeb3 = ({ nav, ctaLabel }: PhoneScreenProps) => { >
-

Your collection (1)

+

+ {t("sim-cw-your-collection", { count: 1 })} +

{ alt="NFT Image" /> -

Cool art

+

+ {t("sim-cw-nft-title")} +

diff --git a/src/components/Simulator/screens/CreateAccount/GeneratingKeys.tsx b/src/components/Simulator/screens/CreateAccount/GeneratingKeys.tsx index d359c6bfc33..4d0e0826f43 100644 --- a/src/components/Simulator/screens/CreateAccount/GeneratingKeys.tsx +++ b/src/components/Simulator/screens/CreateAccount/GeneratingKeys.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react" import { Check } from "lucide-react" import { motion } from "motion/react" +import { useTranslations } from "next-intl" import type { PhoneScreenProps } from "@/lib/types" @@ -26,6 +27,7 @@ export const GeneratingKeys = ({ ctaLabel, generateNewWords, }: GeneratingKeysProps) => { + const t = useTranslations("simulator") const { progressStepper } = nav const [loading, setLoading] = useState(true) const [complete, setComplete] = useState(false) @@ -81,9 +83,7 @@ export const GeneratingKeys = ({ )}

- {loading - ? "Generating example recovery phrase" - : "Example account created"} + {loading ? t("sim-ca-generating") : t("sim-ca-account-created")}

{complete && ( } -export const InitialWordDisplay = ({ words }: InitialWordDisplayProps) => ( -
-
-

- Recovery phrase example -

+export const InitialWordDisplay = ({ words }: InitialWordDisplayProps) => { + const t = useTranslations("simulator") + + return ( +
+
+

+ {t("sim-ca-recovery-example")} +

+
+
- -
-) + ) +} diff --git a/src/components/Simulator/screens/CreateAccount/InteractiveWordSelector.tsx b/src/components/Simulator/screens/CreateAccount/InteractiveWordSelector.tsx index 98a5b2a5ac7..ebd2d5f06f2 100644 --- a/src/components/Simulator/screens/CreateAccount/InteractiveWordSelector.tsx +++ b/src/components/Simulator/screens/CreateAccount/InteractiveWordSelector.tsx @@ -1,4 +1,5 @@ import { useState } from "react" +import { useTranslations } from "next-intl" import { PhoneScreenProps } from "@/lib/types" @@ -16,12 +17,13 @@ export const InteractiveWordSelector = ({ ctaLabel, nav, }: InteractiveWordSelectorProps) => { + const t = useTranslations("simulator") const { progressStepper } = nav const [wordsSelected, setWordsSelected] = useState(0) return (

- Repeat the words + {t("sim-ca-repeat-words")}

{wordsSelected < words.length ? ( diff --git a/src/components/Simulator/screens/CreateAccount/RecoveryPhraseNotice.tsx b/src/components/Simulator/screens/CreateAccount/RecoveryPhraseNotice.tsx index 1c3fd4016a5..bfc0856b4bd 100644 --- a/src/components/Simulator/screens/CreateAccount/RecoveryPhraseNotice.tsx +++ b/src/components/Simulator/screens/CreateAccount/RecoveryPhraseNotice.tsx @@ -1,18 +1,26 @@ import { motion } from "motion/react" +import { useTranslations } from "next-intl" -export const RecoveryPhraseNotice = () => ( - -

Recovery phrase

-

- Any person knowing this secret recovery phrase can make - transactions on behalf of your account. -

-

Wallet app providers do not have access to your account—only you do.

-

You must back it up safely.

-
-) +export const RecoveryPhraseNotice = () => { + const t = useTranslations("simulator") + + return ( + +

+ {t("sim-ca-recovery-title")} +

+

+ {t.rich("sim-ca-recovery-desc-1", { + strong: (chunks) => {chunks}, + })} +

+

{t("sim-ca-recovery-desc-2")}

+

{t("sim-ca-recovery-desc-3")}

+
+ ) +} diff --git a/src/components/Simulator/screens/CreateAccount/WelcomeScreen.tsx b/src/components/Simulator/screens/CreateAccount/WelcomeScreen.tsx index 09e7c51f31c..078ba053783 100644 --- a/src/components/Simulator/screens/CreateAccount/WelcomeScreen.tsx +++ b/src/components/Simulator/screens/CreateAccount/WelcomeScreen.tsx @@ -1,4 +1,5 @@ import { motion } from "motion/react" +import { useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" @@ -6,17 +7,21 @@ import { EthGlyphIcon } from "../../icons" const MotionFlex = motion.create(Flex) -export const WelcomeScreen = () => ( - - -

- Welcome to - wallet simulator -

-
-) +export const WelcomeScreen = () => { + const t = useTranslations("simulator") + + return ( + + +

+ {t("sim-ca-welcome-to")} + {t("sim-ca-wallet-simulator")} +

+
+ ) +} diff --git a/src/data/WalletSimulatorData.tsx b/src/data/WalletSimulatorData.tsx index 384808b1579..7ccd0951d78 100644 --- a/src/data/WalletSimulatorData.tsx +++ b/src/data/WalletSimulatorData.tsx @@ -47,121 +47,94 @@ export function useWalletOnboardingSimData(): SimulatorData { export function buildSimulatorData(t: TranslateFn): SimulatorData { return { [CREATE_ACCOUNT]: { - title: "Create account", + title: t("sim-ca-title"), Icon: CreateAccountIcon, Screen: CreateAccount, explanations: [ { - header: "Begin your journey by downloading a wallet", + header: t("sim-ca-header-1"), description: ( <> -

To get started, you'll need to download a wallet app.

-

- Most people use mobile apps, but desktop apps and browser - extensions are also available. -

-

- Let's set up a mobile wallet. Click "Install a - wallet" to get started. -

+

{t("sim-ca-desc-1-p1")}

+

{t("sim-ca-desc-1-p2")}

+

{t("sim-ca-desc-1-p3")}

), }, { - header: "Wallets are free apps you can download", + header: t("sim-ca-header-2"), description: ( <> -

- Mobile wallet apps can be downloaded and installed using any app - store. -

-

- Wallets provide an easy way to create an Ethereum account, and - then use Ethereum and its applications. -

-

Go ahead and open your new wallet app.

+

{t("sim-ca-desc-2-p1")}

+

{t("sim-ca-desc-2-p2")}

+

{t("sim-ca-desc-2-p3")}

), }, { - header: "Creating an account is free, private and easy", + header: t("sim-ca-header-3"), description: ( <> -

- Ethereum accounts are created privately and do not require any - forms or approval—no personal identifying information required! -

-

- Click on "Create account" to generate a new account. -

+

{t("sim-ca-desc-3-p1")}

+

{t("sim-ca-desc-3-p2")}

), }, { - header: - "This is YOUR account, and nobody else's—you control it completely", - description: ( -

- No company, including your wallet provider, has access to your - account. -

- ), + header: t("sim-ca-header-4"), + description:

{t("sim-ca-desc-4-p1")}

, }, { - header: "A recovery phrase is used to keep the account safe", + header: t("sim-ca-header-5"), description: ( <> -

- You and only you control this phrase, so it is critical to take - steps to backup and secure it. -

-

- Read carefully and click "I understand" to see and - backup your recovery phrase. -

+

{t("sim-ca-desc-5-p1")}

+

{t("sim-ca-desc-5-p2")}

), }, { - header: "Keep your phrase safe!", + header: t("sim-ca-header-6"), description: ( <> -

Storing small amount of value:

+

{t("sim-ca-desc-6-small-title")}

- Consider saving in a - password manager + {" "} + {t("sim-ca-desc-6-small-1")}
-

Storing any significant value:

+

+ {t("sim-ca-desc-6-significant-title")} +

- Write your recovery - phrase down + {" "} + {t("sim-ca-desc-6-significant-1")} - Store it in a safe - place (consider multiple backups) + {" "} + {t("sim-ca-desc-6-significant-2")}
-

Unsafe backup methods:

+

{t("sim-ca-desc-6-unsafe-title")}

- Texting it to a friend (or anyone!) + {t("sim-ca-desc-6-unsafe-1")} - Taking a picture of the phrase + {t("sim-ca-desc-6-unsafe-2")} - Saving it in a file on your computer + {t("sim-ca-desc-6-unsafe-3")}
@@ -169,45 +142,40 @@ export function buildSimulatorData(t: TranslateFn): SimulatorData { ), }, { - header: "Repeat phrase to prove you have saved it", + header: t("sim-ca-header-7"), description: ( <>

- This is done on initial setup only, but is not{" "} - required every time. -

-

- Keep this private! Nobody from customer service - should ever ask you for this. + {t.rich("sim-ca-desc-7-p1", { + strong: (chunks) => {chunks}, + })}

- Click the words in the correct order to prove you've backed - up your phrase. + {t.rich("sim-ca-desc-7-p2", { + strong: (chunks) => {chunks}, + em: (chunks) => {chunks}, + })}

+

{t("sim-ca-desc-7-p3")}

), }, { - header: "That's it! Welcome to Ethereum 🎉", - description: ( -

- In the next lesson we'll learn how to use your new account to - receive and send some funds. -

- ), + header: t("sim-ca-header-8"), + description:

{t("sim-ca-desc-8-p1")}

, }, ], ctaLabels: [ - "Install a wallet", - "Open wallet", - "Create account", - "Next", - "I understand", - "Next", - "Start using wallet", + t("sim-ca-cta-1"), + t("sim-ca-cta-2"), + t("sim-ca-cta-3"), + t("sim-ca-cta-4"), + t("sim-ca-cta-5"), + t("sim-ca-cta-6"), + t("sim-ca-cta-7"), ], finalCtaLink: { - label: "Download a real wallet", + label: t("sim-ca-final-cta"), href: "/wallets/find-wallet/", }, nextPathId: SEND_RECEIVE, @@ -308,112 +276,78 @@ export function buildSimulatorData(t: TranslateFn): SimulatorData { nextPathId: CONNECT_WEB3, }, [CONNECT_WEB3]: { - title: "Connect to Web3", + title: t("sim-cw-title"), Icon: ConnectWeb3Icon, Screen: ConnectWeb3, explanations: [ { - header: - "Explore Web3: from NFTs to decentralized finance and identity", + header: t("sim-cw-header-1"), description: ( <> -

- Your wallet can be used to connect to all sorts of applications, - allowing you to interact with your onchain assets. -

-

- Your friend just sent an NFT art piece to your address! - Let's go to a new NFT marketplace website to view it. -

+

{t("sim-cw-desc-1-p1")}

+

{t("sim-cw-desc-1-p2")}

), }, { - header: "No need to create a new account for each service", + header: t("sim-cw-header-2"), description: ( <> -

- Your account is universal across all Ethereum and - Ethereum-compatible applications. -

-

Assets stored onchain can be accessed from any application.

+

{t("sim-cw-desc-2-p1")}

+

{t("sim-cw-desc-2-p2")}

), }, { - header: - "You can have a single login for most Ethereum based projects", + header: t("sim-cw-header-3"), description: ( <> -

- The same account address will represent your identity on many - different Ethereum compatible blockchains such as Arbitrum, - Polygon or Optimism. -

-

- Logins are handled by your wallet—no more creating insecure - passwords. -

+

{t("sim-cw-desc-3-p1")}

+

{t("sim-cw-desc-3-p2")}

), }, { - header: "Personal identifying information is not shared", + header: t("sim-cw-header-4"), description: ( <> -

Your private information stays private.

-

- Your personal information, such as email or phone number, is not - needed to use Web3 apps—you only need a wallet. -

-

- Also note there are no associated transaction fees here—signing - in using Ethereum is free, fast and easy! -

+

{t("sim-cw-desc-4-p1")}

+

{t("sim-cw-desc-4-p2")}

+

{t("sim-cw-desc-4-p3")}

), }, { - header: - "No geographical or political discrimination against who can use Ethereum services", + header: t("sim-cw-header-5"), description: ( <> -

There's the NFT you received!

+

{t("sim-cw-desc-5-p1")}

- Wallets are technically only an interface to show you your - balance and to make transactions— - - your assets aren't stored inside the wallet, but on the - blockchain. - + {t.rich("sim-cw-desc-5-p2", { + strong: (chunks) => {chunks}, + })}

), }, { - header: "Start your journey now", + header: t("sim-cw-header-6"), description: ( <> -

- Great job! You're ready to start using apps on Ethereum. -

+

{t("sim-cw-desc-6-p1")}

-

What to do next:

+

{t("sim-cw-desc-6-next")}

- - Learn about staying safe in Web3 - + {t("sim-cw-desc-6-link-1")} - Learn more about Ethereum + {t("sim-cw-desc-6-link-2")} - - Check out some beginner friendly apps - + {t("sim-cw-desc-6-link-3")}
@@ -422,14 +356,14 @@ export function buildSimulatorData(t: TranslateFn): SimulatorData { }, ], ctaLabels: [ - "Visit NFT market", - "Connect wallet", - "Connect to app", - "Go to account", - "Finished", + t("sim-cw-cta-1"), + t("sim-cw-cta-2"), + t("sim-cw-cta-3"), + t("sim-cw-cta-4"), + t("sim-cw-cta-5"), ], finalCtaLink: { - label: "Get a wallet", + label: t("sim-cw-final-cta"), href: "/wallets/find-wallet/", isPrimary: true, }, diff --git a/src/intl/en/simulator.json b/src/intl/en/simulator.json index cb711042dc1..693ba8492b4 100644 --- a/src/intl/en/simulator.json +++ b/src/intl/en/simulator.json @@ -9,7 +9,60 @@ "sim-no-nfts": "No NFTs yet!", "sim-example-walkthrough": "Example walkthrough", "sim-address-tooltip": "Share your address (public identifier) from your own wallet when finished here", + "sim-try-real-app": "Try out a real Ethereum application when finished here", + "sim-try-login-app": "Try logging into a real app with your wallet when finished here", "sim-contact-last-action": "Received 10 minutes ago", + "sim-ca-title": "Create account", + "sim-ca-header-1": "Begin your journey by downloading a wallet", + "sim-ca-desc-1-p1": "To get started, you'll need to download a wallet app.", + "sim-ca-desc-1-p2": "Most people use mobile apps, but desktop apps and browser extensions are also available.", + "sim-ca-desc-1-p3": "Let's set up a mobile wallet. Click \"Install a wallet\" to get started.", + "sim-ca-header-2": "Wallets are free apps you can download", + "sim-ca-desc-2-p1": "Mobile wallet apps can be downloaded and installed using any app store.", + "sim-ca-desc-2-p2": "Wallets provide an easy way to create an Ethereum account, and then use Ethereum and its applications.", + "sim-ca-desc-2-p3": "Go ahead and open your new wallet app.", + "sim-ca-header-3": "Creating an account is free, private and easy", + "sim-ca-desc-3-p1": "Ethereum accounts are created privately and do not require any forms or approval—no personal identifying information required!", + "sim-ca-desc-3-p2": "Click on \"Create account\" to generate a new account.", + "sim-ca-header-4": "This is YOUR account, and nobody else's—you control it completely", + "sim-ca-desc-4-p1": "No company, including your wallet provider, has access to your account.", + "sim-ca-header-5": "A recovery phrase is used to keep the account safe", + "sim-ca-desc-5-p1": "You and only you control this phrase, so it is critical to take steps to backup and secure it.", + "sim-ca-desc-5-p2": "Read carefully and click \"I understand\" to see and backup your recovery phrase.", + "sim-ca-header-6": "Keep your phrase safe!", + "sim-ca-desc-6-small-title": "Storing small amount of value:", + "sim-ca-desc-6-small-1": "Consider saving in a password manager", + "sim-ca-desc-6-significant-title": "Storing any significant value:", + "sim-ca-desc-6-significant-1": "Write your recovery phrase down", + "sim-ca-desc-6-significant-2": "Store it in a safe place (consider multiple backups)", + "sim-ca-desc-6-unsafe-title": "Unsafe backup methods:", + "sim-ca-desc-6-unsafe-1": "Texting it to a friend (or anyone!)", + "sim-ca-desc-6-unsafe-2": "Taking a picture of the phrase", + "sim-ca-desc-6-unsafe-3": "Saving it in a file on your computer", + "sim-ca-header-7": "Repeat phrase to prove you have saved it", + "sim-ca-desc-7-p1": "This is done on initial setup only, but is not required every time.", + "sim-ca-desc-7-p2": "Keep this private! Nobody from customer service should ever ask you for this.", + "sim-ca-desc-7-p3": "Click the words in the correct order to prove you've backed up your phrase.", + "sim-ca-header-8": "That's it! Welcome to Ethereum 🎉", + "sim-ca-desc-8-p1": "In the next lesson we'll learn how to use your new account to receive and send some funds.", + "sim-ca-cta-1": "Install a wallet", + "sim-ca-cta-2": "Open wallet", + "sim-ca-cta-3": "Create account", + "sim-ca-cta-4": "Next", + "sim-ca-cta-5": "I understand", + "sim-ca-cta-6": "Next", + "sim-ca-cta-7": "Start using wallet", + "sim-ca-final-cta": "Download a real wallet", + "sim-ca-welcome-to": "Welcome to", + "sim-ca-wallet-simulator": "wallet simulator", + "sim-ca-generating": "Generating example recovery phrase", + "sim-ca-account-created": "Example account created", + "sim-ca-recovery-title": "Recovery phrase", + "sim-ca-recovery-desc-1": "Any person knowing this secret recovery phrase can make transactions on behalf of your account.", + "sim-ca-recovery-desc-2": "Wallet app providers do not have access to your account—only you do.", + "sim-ca-recovery-desc-3": "You must back it up safely.", + "sim-ca-recovery-example": "Recovery phrase example", + "sim-ca-repeat-words": "Repeat the words", "sim-sr-title": "Send / receive tokens", "sim-sr-header-1": "Receive digital assets from anywhere", "sim-sr-desc-1-p1": "Your wallet helps you manage your funds, NFTs, Web3 identity and more. Here we'll go over how to receive and send some tokens on Ethereum.", @@ -63,5 +116,49 @@ "sim-summary-arrival-est": "est. about 12 seconds", "sim-summary-fees": "Network fees", "sim-success-sending": "Sending transaction", - "sim-success-sent": "You sent {ethAmount} ETH ({usdAmount}) to {recipient}" + "sim-success-sent": "You sent {ethAmount} ETH ({usdAmount}) to {recipient}", + "sim-cw-title": "Connect to Web3", + "sim-cw-header-1": "Explore Web3: from NFTs to decentralized finance and identity", + "sim-cw-desc-1-p1": "Your wallet can be used to connect to all sorts of applications, allowing you to interact with your onchain assets.", + "sim-cw-desc-1-p2": "Your friend just sent an NFT art piece to your address! Let's go to a new NFT marketplace website to view it.", + "sim-cw-header-2": "No need to create a new account for each service", + "sim-cw-desc-2-p1": "Your account is universal across all Ethereum and Ethereum-compatible applications.", + "sim-cw-desc-2-p2": "Assets stored onchain can be accessed from any application.", + "sim-cw-header-3": "You can have a single login for most Ethereum based projects", + "sim-cw-desc-3-p1": "The same account address will represent your identity on many different Ethereum compatible blockchains such as Arbitrum, Polygon or Optimism.", + "sim-cw-desc-3-p2": "Logins are handled by your wallet—no more creating insecure passwords.", + "sim-cw-header-4": "Personal identifying information is not shared", + "sim-cw-desc-4-p1": "Your private information stays private.", + "sim-cw-desc-4-p2": "Your personal information, such as email or phone number, is not needed to use Web3 apps—you only need a wallet.", + "sim-cw-desc-4-p3": "Also note there are no associated transaction fees here—signing in using Ethereum is free, fast and easy!", + "sim-cw-header-5": "No geographical or political discrimination against who can use Ethereum services", + "sim-cw-desc-5-p1": "There's the NFT you received!", + "sim-cw-desc-5-p2": "Wallets are technically only an interface to show you your balance and to make transactions—your assets aren't stored inside the wallet, but on the blockchain.", + "sim-cw-header-6": "Start your journey now", + "sim-cw-desc-6-p1": "Great job! You're ready to start using apps on Ethereum.", + "sim-cw-desc-6-next": "What to do next:", + "sim-cw-desc-6-link-1": "Learn about staying safe in Web3", + "sim-cw-desc-6-link-2": "Learn more about Ethereum", + "sim-cw-desc-6-link-3": "Check out some beginner friendly apps", + "sim-cw-cta-1": "Visit NFT market", + "sim-cw-cta-2": "Connect wallet", + "sim-cw-cta-3": "Connect to app", + "sim-cw-cta-4": "Go to account", + "sim-cw-cta-5": "Finished", + "sim-cw-final-cta": "Get a wallet", + "sim-cw-welcome-web3": "Welcome to Web3", + "sim-cw-nft-marketplace": "NFT Marketplace", + "sim-cw-connect-wallet-prompt": "Connect your wallet to view your collection", + "sim-cw-connect-disclaimer": "Connecting to the website will not share any personal or secure information with the site owners.", + "sim-cw-your-collection": "Your collection ({count})", + "sim-cw-nft-title": "Cool art", + "sim-cw-nft-actions-tooltip": "These are some things you could do as the owner of your NFTs", + "sim-cw-set-price": "Set a price", + "sim-cw-auction-item": "Auction item", + "sim-cw-transfer-item": "Transfer item", + "sim-cw-browse-artwork": "Browse other artwork", + "sim-cw-mint-nft": "Mint new NFT", + "sim-cw-logged-in": "You're logged in!", + "sim-cw-connect-account": "Connect account?", + "sim-cw-search-website": "Search or enter website" } diff --git a/src/intl/es/page-index.json b/src/intl/es/page-index.json index eed909b1cda..0bfcb944c83 100644 --- a/src/intl/es/page-index.json +++ b/src/intl/es/page-index.json @@ -1,140 +1,121 @@ { - "page-index-activity-description": "Ethereum es la plataforma líder para emitir, gestionar y liquidar activos digitales. Desde dinero tokenizado e instrumentos financieros hasta activos del mundo real y mercados emergentes, Ethereum proporciona una base segura y neutral para la economía digital.", - "page-index-activity-subtitle": "Actividad en la red principal de Ethereum y en las redes de capa 2", - "page-index-activity-tag": "Actividad", - "page-index-activity-header": "El ecosistema más resistente", - "page-index-activity-action": "Más recursos del ecosistema", - "page-index-activity-action-primary": "Ethereum para instituciones", - "page-index-bento-header": "Una nueva forma de utilizar Internet", - "page-index-bento-assets-action": "Más información acerca de NFT", - "page-index-bento-assets-content": "Desde el arte hasta los bienes raíces y las acciones, cualquier activo puede ser tokenizado en Ethereum para probar y verificar la propiedad digitalmente. Compre, venda, intercambie y cree activos y coleccionables en cualquier momento y en cualquier lugar.", - "page-index-bento-assets-title": "El Internet de los activos", - "page-index-bento-dapps-action": "Explorar apps", - "page-index-bento-dapps-content": "Las aplicaciones creadas en Ethereum funcionan sin vender sus datos. Desde redes sociales hasta juegos o trabajo, utilice la misma cuenta para cada aplicación innovadora mientras mantiene la privacidad y el acceso.", - "page-index-bento-dapps-title": "Aplicaciones que respetan su privacidad", - "page-index-bento-defi-action": "Explorar DeFi", - "page-index-bento-defi-content": "Pida prestado, preste, gane intereses y mucho más, sin necesidad de una cuenta bancaria. El sistema financiero descentralizado de Ethereum está abierto 24/7 para cualquier persona con conexión a internet.", - "page-index-bento-defi-title": "Un sistema financiero abierto a todos", - "page-index-bento-networks-action": "Descubra las capas 2", - "page-index-bento-networks-content": "Cientos de redes de capa 2 están construidas sobre Ethereum. Disfrute de comisiones bajas y transacciones casi instantáneas mientras se beneficia de la seguridad probada de Ethereum.", - "page-index-bento-networks-title": "La red de redes", - "page-index-bento-stablecoins-action": "Descubra las monedas estables", - "page-index-bento-stablecoins-content": "Las stablecoins son monedas que mantienen un precio estable, equiparado a activos estables como el dólar estadounidense. Acceda a pagos globales al instante o almacene valor en dólares digitales en Ethereum.", - "page-index-bento-stablecoins-title": "Dinero digital para el uso diario", - "page-index-builders-action-primary": "Portal para desarrolladores", - "page-index-builders-action-secondary": "Documentación", - "page-index-builders-description": "Ethereum alberga el ecosistema de desarrolladores más grande y dinámico de la Web3. Use JavaScript y Python, o aprenda un lenguaje de contratos inteligentes como Solidity o Vyper para crear su propia aplicación.", - "page-index-builders-tag": "Creadores", - "page-index-builders-header": "La comunidad de creadores de cadena de bloques más grande del mundo", - "page-index-calendar-add": "Añadir al calendario", - "page-index-calendar-fallback": "No hay próximas reuniones", - "page-index-calendar-title": "Próximas llamadas", - "page-index-community-action": "Más de ethereum.org", - "page-index-community-description-1": "Cientos de traductores, codificadores, diseñadores, redactores y miembros entusiastas de la comunidad crean y mantienen el sitio web ethereum.org cada mes.", - "page-index-community-description-2": "Haga preguntas, conéctese con personas de todo el mundo y contribuya al sitio web. ¡Obtendrá experiencia práctica relevante y recibirá orientación durante el proceso!", - "page-index-community-description-3": "La comunidad de Ethereum.org es el lugar ideal para comenzar y aprender.", - "page-index-community-tag": "Comunidad de Ethereum.org", - "page-index-community-header": "Creada por la comunidad", - "page-index-cta-dapps-description": "Finanzas, juegos e interacción social", - "page-index-cta-dapps-label": "Pruebe apps", - "page-index-cta-get-eth-description": "La moneda de Ethereum", - "page-index-cta-get-eth-label": "Consiga ETH", - "page-index-cta-wallet-description": "Cree cuentas y administre activos", - "page-index-cta-wallet-label": "Seleccione una billetera", - "page-index-cta-build-apps-description": "Cree su primera aplicación", - "page-index-cta-build-apps-label": "Empezar a crear", - "page-index-description": "La plataforma líder para aplicaciones innovadoras y redes de cadena de bloques", - "page-index-developers-code-example-description-0": "Construya un banco impulsado por la lógica que programe", - "page-index-developers-code-example-description-1": "Cree tókenes que pueda transferir y usar en distintas aplicaciones", - "page-index-developers-code-example-description-2": "Utilice lenguajes existentes para interactuar con Ethereum y otras aplicaciones", - "page-index-developers-code-example-description-3": "Reimagine los servicios existentes como aplicaciones abiertas y descentralizadas", - "page-index-developers-code-example-title-0": "Su propio banco", - "page-index-developers-code-example-title-1": "Su propia moneda", - "page-index-developers-code-example-title-2": "Una cartera Ethereum en JavaScript", - "page-index-developers-code-example-title-3": "Un DNS abierto y sin permisos", - "page-index-developers-code-examples": "Ejemplos de código", - "page-index-events-action": "Ver todos los eventos", - "page-index-events-header": "Eventos de Ethereum", - "page-index-events-subtitle": "Las comunidades de Ethereum organizan eventos en todo el mundo, durante todo el año", - "page-index-hero-image-alt": "Una ilustración de una ciudad futurista, que representa el ecosistema Ethereum.", - "page-index-join-action-contribute-description": "Descubra todas las diferentes formas en que puede ayudar a que ethereum.org crezca y mejore.", - "page-index-join-action-contribute-label": "Cómo contribuir", - "page-index-join-action-discord-description": "Para hacer preguntas, coordinar contribuciones y unirse a las llamadas de la comunidad.", - "page-index-join-action-github-description": "Contribuya en el código, diseño, artículos, etc.", - "page-index-join-action-twitter-description": "Para mantenerse al día con nuestras actualizaciones y noticias importantes.", - "page-index-join-description": "El sitio web ethereum.org está construido y mantenido por miles de traductores, programadores, diseñadores, redactores y miembros de la comunidad. Puede proponer ediciones a cualquiera de los contenidos de este sitio de código abierto.", - "page-index-join-header": "Únase a ethereum.org", - "page-index-join-action-hub": "Centro de colaboradores de ethereum.org", - "page-index-learn-description": "Ethereum es una red blockchain descentralizada y una plataforma de desarrollo de software, impulsada por la criptomoneda ether (ETH). Estos recursos son su puerta de entrada para navegar, comprender y utilizar Ethereum con confianza.", - "page-index-what-is-ethereum-title": "¿Qué es Ethereum?", - "page-index-what-is-ethereum-description-1": "Ethereum es una red blockchain de código abierto descentralizada y una plataforma de desarrollo de software, impulsada por la criptomoneda ether (ETH). Ethereum es la base segura y global para una nueva generación de aplicaciones imparables.", - "page-index-what-is-ethereum-description-2": "La red de Ethereum está abierta a todo el mundo: no se necesita ningún permiso. No tiene propietario y está construida y mantenida por miles de personas, organizaciones y usuarios de todo el mundo.", - "page-index-what-is-ethereum-action": "Obtener información sobre Ethereum", - "page-index-what-is-ether-title": "¿Qué es el ETH?", - "page-index-what-is-ether-description-1": "Ether (ETH) es la criptomoneda nativa que impulsa la red de Ethereum, utilizada para pagar las comisiones de transacción y asegurar la blockchain mediante el staking.", - "page-index-what-is-ether-description-2": "Más allá de su función técnica, ETH es dinero digital abierto y programable. Se utiliza para pagos globales, como garantía para préstamos y como reserva de valor que no depende de ninguna entidad central.", - "page-index-what-is-ether-action": "Obtenga más información sobre el ether", - "page-index-learn-tag": "Aprender", - "page-index-network-tag": "Red", - "page-index-token-tag": "Token", - "page-index-learn-header": "Comprenda Ethereum", - "page-index-meta-description": "Ethereum es una plataforma mundial descentralizada para el dinero y nuevos tipos de aplicaciones. En Ethereum, se pueden escribir códigos que controlan el dinero y construir aplicaciones accesibles desde cualquier rincón del mundo.", "page-index-meta-title": "Ethereum.org: La guía completa sobre Ethereum", - "page-index-network-stats-total-eth-staked": "Valor que protege Ethereum", - "page-index-network-stats-tx-cost-description": "Costo de transacción promedio", - "page-index-network-stats-tx-day-description": "Transacciones en las últimas 24 h", - "page-index-network-stats-value-defi-description": "Valor bloqueado en DeFi", - "page-index-network-stats-total-value-held": "Valor total mantenido en Ethereum", - "page-index-popular-topics-ethereum": "¿Qué es Ethereum?", - "page-index-popular-topics-header": "Temas populares", - "page-index-popular-topics-action": "Más guías en el Centro de aprendizaje de Ethereum", - "page-index-popular-topics-roadmap": "Hoja de ruta de Ethereum", - "page-index-popular-topics-start": "Guías paso a paso de Ethereum", - "page-index-popular-topics-wallets": "¿Qué son las carteras de criptomonedas?", - "page-index-popular-topics-whitepaper": "Informe oficial de Ethereum", - "page-index-posts-action": "Leer más sobre estos sitios web", - "page-index-posts-header": "Noticias de Ethereum", - "page-index-posts-subtitle": "Las últimas publicaciones de blog y actualizaciones de la comunidad", + "page-index-meta-description": "Ethereum es una plataforma mundial descentralizada para el dinero y nuevos tipos de aplicaciones. En Ethereum, se pueden escribir códigos que controlan el dinero y construir aplicaciones accesibles desde cualquier rincón del mundo.", + "page-index-description": "La plataforma líder para aplicaciones innovadoras y redes de cadena de bloques", "page-index-title": "Bienvenidos a Ethereum", - "page-index-use-cases-tag": "Casos de uso", - "page-index-values-description": "Sea parte de la revolución digital", - "page-index-values-header": "Internet está cambiando", - "page-index-values-legacy": "Legado", - "page-index-values-tag": "Valores", - "page-index-values-ownership-legacy-label": "Propiedad restringida", - "page-index-values-ownership-legacy-content-0": "En un banco o una plataforma de redes sociales tradicionales, la organización administra sus activos y datos, y usted confía en ellos para acceder a estos y controlarlos.", - "page-index-values-ownership-legacy-content-1": "Es posible que utilicen sus datos de maneras con las que usted no esté de acuerdo, según sus políticas.", - "page-index-values-ownership-ethereum-label": "Propiedad directa", - "page-index-values-ownership-ethereum-content-0": "Con Ethereum, solo usted tiene acceso y control. Nadie más debería poder usar sus activos. Puede decidir a quién conceder ese permiso.", - "page-index-values-fairness-legacy-label": "Sistema discriminatorio", - "page-index-values-fairness-legacy-content-0": "Hoy en día, no todo el mundo tiene el mismo acceso a los servicios financieros. Algunas personas pueden enfrentarse a barreras de acceso debido a su ubicación o nacionalidad.", - "page-index-values-fairness-ethereum-label": "Acceso igualitario", - "page-index-values-fairness-ethereum-content-0": "Creemos que todo el mundo debería poder beneficiarse de un sistema global. Por eso, Ethereum otorga acceso igualitario a todos en todo el mundo, independientemente de quién sea o de dónde venga.", - "page-index-values-privacy-legacy-label": "Sin privacidad", - "page-index-values-privacy-legacy-content-0": "No podemos esperar que los gobiernos, corporaciones u otras grandes organizaciones sin rostro nos concedan privacidad por su beneficencia.", - "page-index-values-privacy-legacy-content-1": "La mayoría de las aplicaciones recopilan la mayor cantidad posible de información personal para poder enviarle marketing personalizado.", - "page-index-values-privacy-ethereum-label": "Privacidad orientada", - "page-index-values-privacy-ethereum-content-0": "La comunidad de Ethereum respeta la privacidad. Tiene derecho a usar aplicaciones sin revelar su identidad ni proporcionar sus datos de contacto.", - "page-index-values-integration-legacy-label": "Fragmentación", - "page-index-values-integration-legacy-content-0": "La mayoría de las aplicaciones lo presionan para crear cuentas separadas, lo que hace que sea difícil recordar todos sus datos de inicio de sesión y registros.", - "page-index-values-integration-ethereum-label": "Integración", - "page-index-values-integration-ethereum-content-0": "Con Ethereum puede reutilizar una cuenta en todas las aplicaciones. No es necesario registrarse individualmente.", - "page-index-values-decentralization-legacy-label": "Centralizado", - "page-index-values-decentralization-legacy-content-0": "Las empresas son propiedad de empresarios y accionistas privados. Solo ellos ejercen el control sobre la empresa y son los que más se benefician de su éxito.", - "page-index-values-decentralization-ethereum-label": "Sistema descentralizado", - "page-index-values-decentralization-ethereum-content-0": "Al igual que Internet, Ethereum no pertenece a nadie. Es un recurso compartido que todos pueden controlar de forma equitativa. No existe un único propietario que pueda controlarlo.", - "page-index-values-censorship-legacy-label": "Censurable", - "page-index-values-censorship-legacy-content-0": "Las plataformas modernas y sus reglas cambian con frecuencia. Pueden verse influidas por las partes interesadas, la dirección de la empresa o incluso por regímenes opresivos.", - "page-index-values-censorship-ethereum-label": "Resistente a la censura", - "page-index-values-censorship-ethereum-content-0": "La resistencia a la opresión es el principio fundamental de Ethereum. Su funcionalidad debe ser siempre justa e imparcial.", - "page-index-values-censorship-ethereum-content-1": "Ethereum no puede ser controlado por ningún estado nacional, empresa ni individuo.", - "page-index-values-open-legacy-label": "Cerrado a la mayoría", - "page-index-values-open-legacy-content-0": "Las empresas protegen su propiedad intelectual y no la comparten. Nadie fuera de la empresa puede ver cómo funcionan las cosas, solucionar problemas o hacer mejoras. Es difícil que las personas creen nuevas herramientas o personalicen las existentes.", - "page-index-values-open-ethereum-label": "Abierto a todos", - "page-index-values-open-ethereum-content-0": "Ethereum es de código abierto. Cualquiera puede ver, usar y mejorar el código, haciéndolo mejor para todos.", + "page-index-hero-image-alt": "Una ilustración de una ciudad futurista, que representa el ecosistema Ethereum.", + "page-index-hero-title": "El internet que te pertenece", + "page-index-hero-subtitle": "Ethereum es la red global donde controlas tus activos, tus datos y tu identidad.", + "page-index-hero-cta": "Empieza aquí", + "page-index-cta-wallet-label": "Elige una billetera", + "page-index-cta-wallet-description": "Crea cuentas y administra activos", + "page-index-cta-get-eth-label": "Consigue ETH", + "page-index-cta-get-eth-description": "La moneda de Ethereum", + "page-index-cta-dapps-label": "Prueba apps", + "page-index-cta-dapps-description": "Descubre lo que Ethereum puede hacer", + "page-index-cta-build-apps-label": "Empieza a crear", + "page-index-cta-build-apps-description": "Crea tu primera aplicación", + "page-index-cta-learn-label": "Aprende Ethereum", + "page-index-modal-title": "¿Qué te trae por aquí?", + "page-index-modal-description": "Elige tu camino: recursos para principiantes, desarrolladores o empresas.", + "page-index-modal-beginners": "Para principiantes", + "page-index-modal-explorers": "Para exploradores", + "page-index-modal-builders": "Para desarrolladores", + "page-index-modal-enterprise": "Para empresas", + "page-index-modal-what-is-ethereum": "¿Qué es Ethereum?", + "page-index-modal-pick-wallet": "Elige una billetera", + "page-index-modal-get-eth": "Consigue ETH", + "page-index-modal-try-apps": "Prueba apps", + "page-index-modal-start-building": "Empieza a crear", + "page-index-modal-docs": "Documentación", + "page-index-modal-founders": "Fundadores", + "page-index-modal-institutions": "Instituciones", + "page-index-kpi-tag": "El internet de los usuarios", + "page-index-kpi-title": "Ethereum te devuelve el control de tus activos", + "page-index-kpi-description": "Tu cuenta bancaria es un registro en la base de datos de otra persona. Tu aplicación es un archivo en el servidor de otra persona. Ethereum es una red alternativa donde posees tus activos directamente.", + "page-index-kpi-holders": "Poseedores de ETH", + "page-index-kpi-transactions": "Transacciones hoy", + "page-index-carousel-privacy-tag": "TU PRIVACIDAD ES TUYA", + "page-index-carousel-privacy-title": "Usa internet sin ser vigilado", + "page-index-carousel-privacy-subtitle": "La mayoría de las apps rastrean lo que haces, con quién hablas y lo que posees. Venden esos datos o los entregan cuando se los piden. En Ethereum, tu actividad puede permanecer privada.", + "page-index-carousel-privacy-description": "Sin cuentas vinculadas a tu nombre. Sin empresas vigilando tu saldo.", + "page-index-carousel-privacy-cta": "Usa apps que preservan tu privacidad →", + "page-index-carousel-privacy-traditional-label": "APPS TRADICIONALES", + "page-index-carousel-privacy-traditional-value": "Tus datos son su producto", + "page-index-carousel-privacy-ethereum-label": "APPS DE ETHEREUM", + "page-index-carousel-privacy-ethereum-value": "Privado por defecto", + "page-index-carousel-remittances-tag": "PAGOS INTERNACIONALES", + "page-index-carousel-remittances-title": "Envía dinero a casa en {minutes} minutos", + "page-index-carousel-remittances-subtitle": "Evita la comisión de {wireFee} y la espera de {days}+ días.", + "page-index-carousel-remittances-description": "Envía stablecoins a cualquier persona, en cualquier parte del mundo, por solo {txFee}. Los fondos llegan casi al instante.", + "page-index-carousel-remittances-cta": "Pruébalo tú mismo →", + "page-index-carousel-remittances-traditional-label": "TRANSFERENCIA BANCARIA", + "page-index-carousel-remittances-traditional-value": "{min}-{max} días", + "page-index-carousel-remittances-ethereum-label": "ETHEREUM", + "page-index-carousel-remittances-ethereum-value": "{minutes} minutos", + "page-index-carousel-borrowing-tag": "ACCESO FINANCIERO", + "page-index-carousel-borrowing-title": "Pide prestado sin historial crediticio", + "page-index-carousel-borrowing-subtitle": "No necesitas un puntaje de crédito para empezar.", + "page-index-carousel-borrowing-description": "Con apps DeFi en Ethereum, puedes aportar colateral y acceder a crédito al instante, sin necesidad de permisos.", + "page-index-carousel-borrowing-cta": "Más información sobre DeFi →", + "page-index-carousel-borrowing-traditional-label": "BANCO TRADICIONAL", + "page-index-carousel-borrowing-traditional-value": "Verificación crediticia", + "page-index-carousel-borrowing-ethereum-label": "EN ETHEREUM", + "page-index-carousel-borrowing-ethereum-value": "Basado en colateral", + "page-index-trust-image-alt": "Ilustración de la comunidad Ethereum", + "page-index-trust-never-offline": "Nunca fuera de línea", + "page-index-trust-uptime": "{uptime} de disponibilidad", + "page-index-trust-years": "{count} años", + "page-index-trust-since": "Desde 2015", + "page-index-trust-tag": "Trayectoria comprobada", + "page-index-trust-title": "Construido para durar", + "page-index-trust-description-1": "Ethereum ha funcionado continuamente desde 2015 sin un solo segundo de inactividad.", + "page-index-trust-description-2": "El código es abierto para que cualquiera lo verifique. Ninguna empresa lo controla, nadie puede apagarlo, y miles de operadores independientes lo mantienen en funcionamiento en todo el mundo.", + "page-index-trust-cta": "Consigue ETH", + "page-index-simulator-tag": "Gratis para siempre", + "page-index-simulator-title": "Prueba Ethereum en tu navegador", + "page-index-simulator-subtitle": "Experimenta cómo funciona Ethereum. Solo haz clic y explora.", + "page-index-features-title": "Lo que hace a Ethereum", + "page-index-features-title-highlight": "diferente", + "page-index-features-subtitle": "Principios que distinguen a Ethereum de los sistemas tradicionales", + "page-index-features-ownership-title": "Propiedad directa", + "page-index-features-ownership-description-1": "Tu saldo bancario es una ", + "page-index-features-ownership-description-custody": "promesa de custodia", + "page-index-features-ownership-description-2": "Tu saldo en Ethereum es propiedad real.", + "page-index-features-ownership-stat": "{volume}+", + "page-index-features-ownership-stat-label": "Volumen diario de transacciones", + "page-index-features-public-rules-title": "Reglas públicas", + "page-index-features-public-rules-description": "El código es público, los acuerdos se ejecutan exactamente como están escritos. Piensa en una máquina expendedora versus esperar que el cajero te dé el cambio correcto.", + "page-index-features-global-title": "Global", + "page-index-features-global-description": "Cualquier persona, en cualquier lugar, puede usar Ethereum. No se necesita permiso.", + "page-index-features-free-access-title": "Acceso libre", + "page-index-features-free-access-description": "Sin verificación de crédito, sin saldo mínimo, sin aprobación de cuenta. Si tienes internet, ya estás dentro.", + "page-index-features-nobody-owns-title": "Nadie es dueño de Ethereum", + "page-index-features-nobody-owns-description": "Los cambios ocurren a través de propuestas abiertas en las que cualquiera puede participar. Piensa en un huerto comunitario versus una granja corporativa.", + "page-index-features-cta": "¿Qué es Ethereum?", + "page-index-get-started-title": "Empieza en Ethereum", + "page-index-get-started-subtitle": "Toma {minutes} minutos para empezar. Sin verificación de crédito, sin papeleo, sin saldo mínimo.", + "page-index-get-started-learn-title": "Entiende Ethereum", + "page-index-get-started-learn-description": "Empieza aquí. Aprende qué es, por qué importa y cómo funciona en un lenguaje sencillo.", + "page-index-get-started-learn-bullet-1": "¿Qué es Ethereum?", + "page-index-get-started-learn-bullet-2": "¿Cómo funcionan las billeteras?", + "page-index-get-started-learn-bullet-3": "DeFi, stablecoins y NFTs explicados", + "page-index-get-started-learn-cta": "Empieza a aprender", + "page-index-get-started-build-title": "Empieza a crear", + "page-index-get-started-build-description": "Para desarrolladores. Accede a documentación, herramientas y tutoriales para crear en Ethereum.", + "page-index-get-started-build-bullet-1": "Documentación para desarrolladores", + "page-index-get-started-build-bullet-2": "Tutoriales de contratos inteligentes", + "page-index-get-started-build-bullet-3": "Herramientas y frameworks de desarrollo", + "page-index-get-started-build-cta": "Ver materiales", + "page-index-get-started-enterprise-title": "Para empresas", + "page-index-get-started-enterprise-description": "Casos de uso empresariales, recursos institucionales y cómo Ethereum puede servir a tu organización.", + "page-index-get-started-enterprise-bullet-1": "Casos de uso empresariales", + "page-index-get-started-enterprise-bullet-2": "Redes privadas y con permisos", + "page-index-get-started-enterprise-bullet-3": "Recursos institucionales", + "page-index-get-started-enterprise-cta": "Explorar para empresas", "page-index-fusaka-network-upgrade": "Actualización de la red", "page-index-fusaka-description": "Para una red de Ethereum más rápida, segura y fácil de usar |", "page-index-fusaka-read-more": "Más información", "page-index-fusaka-going-live-in": "Se activará

en", "page-index-fusaka-live-now": "En directo" -} \ No newline at end of file +} From 7bd93ea147a05a2cd837a53ca83a113c0c52fd5b Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 14:21:57 +0200 Subject: [PATCH 014/109] refactor: replace HomeHero2026 with HomeHero --- app/[locale]/page.tsx | 4 +- src/components/Hero/HomeHero/index.tsx | 144 ++++++++++++-- src/components/Hero/HomeHero2026/index.tsx | 208 --------------------- src/components/Hero/index.ts | 2 +- 4 files changed, 132 insertions(+), 226 deletions(-) delete mode 100644 src/components/Hero/HomeHero2026/index.tsx diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index a3fdf93afeb..aee24524ca0 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -10,7 +10,7 @@ import { import type { PageParams } from "@/lib/types" -import HomeHero2026 from "@/components/Hero/HomeHero2026" +import HomeHero from "@/components/Hero/HomeHero" import FeatureCards from "@/components/Homepage/FeatureCards" import GetStartedGrid from "@/components/Homepage/GetStartedGrid" import TrustLogos from "@/components/Homepage/TrustLogos" @@ -89,7 +89,7 @@ const Page = async (props: { params: Promise }) => { - +
diff --git a/src/components/Hero/HomeHero/index.tsx b/src/components/Hero/HomeHero/index.tsx index d60feaef6f7..b4733615ed8 100644 --- a/src/components/Hero/HomeHero/index.tsx +++ b/src/components/Hero/HomeHero/index.tsx @@ -1,9 +1,24 @@ +import { Fragment, Suspense } from "react" +import dynamic from "next/dynamic" import { getImageProps, type StaticImageData } from "next/image" import { getTranslations } from "next-intl/server" import type { ClassNameProp } from "@/lib/types" +import { ChevronNext } from "@/components/Chevron" import LanguageMorpher from "@/components/Homepage/LanguageMorpher" +import { Button } from "@/components/ui/buttons/Button" + +const PersonaModalCTA = dynamic( + () => import("@/components/Homepage/PersonaModalCTA") +) +import EthGlyphIcon from "@/components/icons/eth-glyph.svg" +import EthTokenIcon from "@/components/icons/eth-token.svg" +import EthWalletIcon from "@/components/icons/eth-wallet.svg" +import TryAppsIcon from "@/components/icons/phone-homescreen.svg" +import SvgButtonLink, { + type SvgButtonLinkProps, +} from "@/components/ui/buttons/SvgButtonLink" import { cn } from "@/lib/utils/cn" import { breakpointAsNumber } from "@/lib/utils/screen" @@ -11,10 +26,14 @@ import { breakpointAsNumber } from "@/lib/utils/screen" import heroBase from "@/public/images/home/hero.png" import hero2xl from "@/public/images/home/hero-2xl.png" +export type CTAVariant = "modal" | "direct-buttons" + type HomeHeroProps = ClassNameProp & { image?: StaticImageData image2xl?: StaticImageData alt?: string + ctaVariant?: CTAVariant + eventCategory?: string } const HomeHero = async ({ @@ -22,13 +41,49 @@ const HomeHero = async ({ image, image2xl, alt: altProp, + ctaVariant = "modal", + eventCategory = "Homepage", }: HomeHeroProps) => { const t = await getTranslations("page-index") - const baseImage = image ?? heroBase const xlImage = image2xl ?? image ?? hero2xl const alt = altProp ?? t("page-index-hero-image-alt") + const directButtonCTAs = [ + { + label: t("page-index-cta-learn-label"), + description: t("page-index-modal-what-is-ethereum"), + href: "/what-is-ethereum/", + Svg: EthGlyphIcon, + className: "text-accent-a hover:text-accent-a-hover", + eventName: "learn_ethereum", + }, + { + label: t("page-index-cta-wallet-label"), + description: t("page-index-cta-wallet-description"), + href: "/wallets/find-wallet/", + Svg: EthWalletIcon, + className: "text-primary hover:text-primary-hover", + eventName: "pick_wallet", + }, + { + label: t("page-index-cta-get-eth-label"), + description: t("page-index-cta-get-eth-description"), + href: "/get-eth/", + Svg: EthTokenIcon, + className: "text-accent-b hover:text-accent-b-hover", + eventName: "get_eth", + }, + { + label: t("page-index-cta-dapps-label"), + description: t("page-index-cta-dapps-description"), + href: "/dapps/", + Svg: TryAppsIcon, + className: "text-accent-c hover:text-accent-c-hover", + eventName: "try_apps", + }, + ] + const common = { alt, sizes: `(max-width: ${breakpointAsNumber["2xl"]}px) 100vw, ${breakpointAsNumber["2xl"]}px`, @@ -53,7 +108,7 @@ const HomeHero = async ({ delete (rest as Record).blurHeight return ( -
+
- {alt} + {alt}
-
-
+ +
+
-
-

{t("page-index-title")}

-

- {t("page-index-description")} + +

+

+ {t("page-index-hero-title")} +

+ +

+ {t("page-index-hero-subtitle")}

+ + {ctaVariant === "modal" ? ( + + {t("page-index-hero-cta")} + + + } + > + + + ) : ( +
+ {directButtonCTAs.map( + ({ + label, + description, + href, + className: ctaClass, + Svg, + eventName, + }) => { + const Link = ( + props: Omit< + SvgButtonLinkProps, + "Svg" | "href" | "label" | "children" + > + ) => ( + +

{description}

+
+ ) + return ( + + + + + ) + } + )} +
+ )}
-
+
) } diff --git a/src/components/Hero/HomeHero2026/index.tsx b/src/components/Hero/HomeHero2026/index.tsx deleted file mode 100644 index 0ef7ec6e032..00000000000 --- a/src/components/Hero/HomeHero2026/index.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { Fragment, Suspense } from "react" -import dynamic from "next/dynamic" -import { getImageProps, type StaticImageData } from "next/image" -import { getTranslations } from "next-intl/server" - -import type { ClassNameProp } from "@/lib/types" - -import { ChevronNext } from "@/components/Chevron" -import LanguageMorpher from "@/components/Homepage/LanguageMorpher" -import { Button } from "@/components/ui/buttons/Button" - -const PersonaModalCTA = dynamic( - () => import("@/components/Homepage/PersonaModalCTA") -) -import EthGlyphIcon from "@/components/icons/eth-glyph.svg" -import EthTokenIcon from "@/components/icons/eth-token.svg" -import EthWalletIcon from "@/components/icons/eth-wallet.svg" -import TryAppsIcon from "@/components/icons/phone-homescreen.svg" -import SvgButtonLink, { - type SvgButtonLinkProps, -} from "@/components/ui/buttons/SvgButtonLink" - -import { cn } from "@/lib/utils/cn" -import { breakpointAsNumber } from "@/lib/utils/screen" - -import heroBase from "@/public/images/home/hero.png" -import hero2xl from "@/public/images/home/hero-2xl.png" - -export type CTAVariant = "modal" | "direct-buttons" - -type HomeHero2026Props = ClassNameProp & { - image?: StaticImageData - image2xl?: StaticImageData - alt?: string - ctaVariant?: CTAVariant - eventCategory?: string -} - -const HomeHero2026 = async ({ - className, - image, - image2xl, - alt: altProp, - ctaVariant = "modal", - eventCategory = "Homepage", -}: HomeHero2026Props) => { - const t = await getTranslations("page-index") - const baseImage = image ?? heroBase - const xlImage = image2xl ?? image ?? hero2xl - const alt = altProp ?? t("page-index-hero-image-alt") - - const directButtonCTAs = [ - { - label: t("page-index-cta-learn-label"), - description: t("page-index-modal-what-is-ethereum"), - href: "/what-is-ethereum/", - Svg: EthGlyphIcon, - className: "text-accent-a hover:text-accent-a-hover", - eventName: "learn_ethereum", - }, - { - label: t("page-index-cta-wallet-label"), - description: t("page-index-cta-wallet-description"), - href: "/wallets/find-wallet/", - Svg: EthWalletIcon, - className: "text-primary hover:text-primary-hover", - eventName: "pick_wallet", - }, - { - label: t("page-index-cta-get-eth-label"), - description: t("page-index-cta-get-eth-description"), - href: "/get-eth/", - Svg: EthTokenIcon, - className: "text-accent-b hover:text-accent-b-hover", - eventName: "get_eth", - }, - { - label: t("page-index-cta-dapps-label"), - description: t("page-index-cta-dapps-description"), - href: "/dapps/", - Svg: TryAppsIcon, - className: "text-accent-c hover:text-accent-c-hover", - eventName: "try_apps", - }, - ] - - const common = { - alt, - sizes: `(max-width: ${breakpointAsNumber["2xl"]}px) 100vw, ${breakpointAsNumber["2xl"]}px`, - priority: true, - } - - const { - props: { srcSet: srcSet2xl }, - } = getImageProps({ ...common, ...xlImage, quality: 20 }) - - const { - props: { srcSet: srcSetMd }, - } = getImageProps({ ...common, ...baseImage, quality: 10 }) - - const { - props: { srcSet: srcSetBase, ...rest }, - } = getImageProps({ ...common, ...baseImage, quality: 5 }) - - // Remove blurWidth/blurHeight from rest to avoid React DOM warnings - // (Next.js getImageProps includes them but they're not valid HTML attributes) - delete (rest as Record).blurWidth - delete (rest as Record).blurHeight - - return ( -
-
- - - - - {alt} - -
- -
-
- - -
-

- {t("page-index-hero-title")} -

- -

- {t("page-index-hero-subtitle")} -

- - {ctaVariant === "modal" ? ( - - {t("page-index-hero-cta")} - - - } - > - - - ) : ( -
- {directButtonCTAs.map( - ({ - label, - description, - href, - className: ctaClass, - Svg, - eventName, - }) => { - const Link = ( - props: Omit< - SvgButtonLinkProps, - "Svg" | "href" | "label" | "children" - > - ) => ( - -

{description}

-
- ) - return ( - - - - - ) - } - )} -
- )} -
-
-
-
- ) -} - -export default HomeHero2026 diff --git a/src/components/Hero/index.ts b/src/components/Hero/index.ts index 48581c76bd0..8df2467aae2 100644 --- a/src/components/Hero/index.ts +++ b/src/components/Hero/index.ts @@ -1,5 +1,5 @@ export { default as ContentHero, type ContentHeroProps } from "./ContentHero" -export { default as HomeHero } from "./HomeHero" +export { type CTAVariant, default as HomeHero } from "./HomeHero" export { default as HubHero } from "./HubHero" export { default as MdxHero, type MdxHeroProps } from "./MdxHero" export { From 8b444b046fa00e9972c41c24003ab258ebbd6f1e Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 14:28:31 +0200 Subject: [PATCH 015/109] style: add text-balance to hero title to prevent orphaned line breaks --- src/components/Hero/HomeHero/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Hero/HomeHero/index.tsx b/src/components/Hero/HomeHero/index.tsx index b4733615ed8..9d44eb1235c 100644 --- a/src/components/Hero/HomeHero/index.tsx +++ b/src/components/Hero/HomeHero/index.tsx @@ -132,7 +132,7 @@ const HomeHero = async ({
-

+

{t("page-index-hero-title")}

From d81025c94c4c15cfba6ee927d51b1891383d00e5 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 15:01:32 +0200 Subject: [PATCH 016/109] remove enterprise category from persona modal --- src/components/Homepage/PersonaModalCTA.tsx | 55 ++------------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/src/components/Homepage/PersonaModalCTA.tsx b/src/components/Homepage/PersonaModalCTA.tsx index ba6a32f7e6a..facf1d22b15 100644 --- a/src/components/Homepage/PersonaModalCTA.tsx +++ b/src/components/Homepage/PersonaModalCTA.tsx @@ -1,13 +1,7 @@ "use client" import { useRef, useState } from "react" -import { - AppWindowMac, - BookOpen, - Building2, - Code, - ExternalLink, -} from "lucide-react" +import { AppWindowMac, BookOpen, Code } from "lucide-react" import { useTranslations } from "next-intl" import { ChevronNext } from "@/components/Chevron" @@ -25,12 +19,9 @@ import { BaseLink } from "@/components/ui/Link" import { cn } from "@/lib/utils/cn" import { trackCustomEvent } from "@/lib/utils/matomo" -import { ENTERPRISE_ETHEREUM_URL } from "@/lib/constants" - type PersonaLink = { label: string href: string - isExternal?: boolean eventName: string } @@ -106,28 +97,7 @@ function useCategories() { }, ] - const enterpriseCategory: PersonaCategory = { - id: "enterprise", - label: t("page-index-modal-enterprise"), - Icon: Building2, - iconBgClass: "bg-accent-c/20", - iconColorClass: "text-accent-c", - links: [ - { - label: t("page-index-modal-founders"), - href: "/founders/", - eventName: "founders", - }, - { - label: t("page-index-modal-institutions"), - href: ENTERPRISE_ETHEREUM_URL, - isExternal: true, - eventName: "institutions", - }, - ], - } - - return { categories, enterpriseCategory, t } + return { categories, t } } const CategoryCard = ({ @@ -159,7 +129,7 @@ const CategoryCard = ({
- {links.map(({ label: linkLabel, href, isExternal, eventName }, idx) => ( + {links.map(({ label: linkLabel, href, eventName }, idx) => (
{idx > 0 &&
} onLinkClick(eventName)} hideArrow className="group flex items-center justify-between text-xl font-bold text-primary no-underline transition-colors hover:text-primary-hover md:text-3xl" - {...(isExternal && { - target: "_blank", - rel: "noopener noreferrer", - })} > - - {linkLabel} - {isExternal && ( - - )} - + {linkLabel}
@@ -191,7 +152,7 @@ type PersonaModalCTAProps = { } const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => { - const { categories, enterpriseCategory, t } = useCategories() + const { categories, t } = useCategories() const [isOpen, setIsOpen] = useState(false) // Track if modal was closed via link click (not ESC/outside click/X button) const closedViaLinkRef = useRef(false) @@ -256,12 +217,6 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => { onLinkClick={handleLinkClick} /> ))} - {/* Enterprise card: mobile only */} -
From 1ea2b02ee367b4d3a65fd0d233fce772d91bbb03 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 15:08:09 +0200 Subject: [PATCH 017/109] fix: disable safari telephone auto-detection via metadata --- src/lib/utils/metadata.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/utils/metadata.ts b/src/lib/utils/metadata.ts index 25ffd020394..cb73858b508 100644 --- a/src/lib/utils/metadata.ts +++ b/src/lib/utils/metadata.ts @@ -95,6 +95,7 @@ export const getMetadata = async ({ const base: Metadata = { title, description, + formatDetection: { telephone: false }, metadataBase: new URL(SITE_URL), alternates: { canonical: url, From a2198fa3b00c6527bad80f4280e227e349aa441d Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 15:58:01 +0200 Subject: [PATCH 018/109] a11y: add aria-labels to swiper nav buttons and carousel landmark --- src/components/Homepage/SavingsCarousel.tsx | 12 ++++++++++-- src/components/ui/swiper.tsx | 17 +++++++++++++---- src/intl/en/page-index.json | 3 +++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index 9935d68018c..14d4c343c01 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -331,6 +331,7 @@ const SavingsCarousel = ({ className, eventCategory = "Homepage", }: SavingsCarouselProps) => { + const t = useTranslations("page-index") const slides = useSlides() const [activeIndex, setActiveIndex] = useState(0) @@ -344,7 +345,11 @@ const SavingsCarousel = ({ } return ( -
+
))} - +
diff --git a/src/components/ui/swiper.tsx b/src/components/ui/swiper.tsx index 22510e5cbf4..bd12b970867 100644 --- a/src/components/ui/swiper.tsx +++ b/src/components/ui/swiper.tsx @@ -112,12 +112,21 @@ SwiperNavContainer.displayName = "SwiperNavContainer" const SwiperNavigation = React.forwardRef< HTMLDivElement, - React.HTMLAttributes ->((props, ref) => ( + React.HTMLAttributes & { + prevLabel?: string + nextLabel?: string + } +>(({ prevLabel, nextLabel, ...props }, ref) => ( - + - + )) SwiperNavigation.displayName = "SwiperNavigation" diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index 5fd09f60835..e1676fddd0f 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -35,6 +35,9 @@ "page-index-kpi-description": "Your bank account is an entry in someone else's database. Your application is a file in someone else's server. Ethereum is an alternative network where you hold your assets directly.", "page-index-kpi-holders": "ETH holders", "page-index-kpi-transactions": "Transactions today", + "page-index-carousel-label": "Ethereum use cases", + "page-index-carousel-previous-slide": "Previous slide", + "page-index-carousel-next-slide": "Next slide", "page-index-carousel-privacy-tag": "YOUR BUSINESS IS YOURS", "page-index-carousel-privacy-title": "Use the internet without being watched", "page-index-carousel-privacy-subtitle": "Most apps track what you do, who you talk to, and what you own. They sell that data or hand it over when asked. On Ethereum, your activity can stay private.", From 6e02c1e9c47ba8457a4223434282e3909b7cea08 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 16:11:57 +0200 Subject: [PATCH 019/109] fix: move buildSimulatorData call to client component to fix wallets build --- app/[locale]/wallets/WalletSimulator.tsx | 12 ++++++++++++ app/[locale]/wallets/page.tsx | 9 +++------ src/data/WalletSimulatorData.tsx | 17 ----------------- 3 files changed, 15 insertions(+), 23 deletions(-) create mode 100644 app/[locale]/wallets/WalletSimulator.tsx diff --git a/app/[locale]/wallets/WalletSimulator.tsx b/app/[locale]/wallets/WalletSimulator.tsx new file mode 100644 index 00000000000..81f2bee140d --- /dev/null +++ b/app/[locale]/wallets/WalletSimulator.tsx @@ -0,0 +1,12 @@ +"use client" + +import { type ReactNode } from "react" + +import { Simulator } from "@/components/Simulator" + +import { useWalletOnboardingSimData } from "@/data/WalletSimulatorData" + +export function WalletSimulator({ children }: { children: ReactNode }) { + const data = useWalletOnboardingSimData() + return {children} +} diff --git a/app/[locale]/wallets/page.tsx b/app/[locale]/wallets/page.tsx index 057932450a5..2ad319176fb 100644 --- a/app/[locale]/wallets/page.tsx +++ b/app/[locale]/wallets/page.tsx @@ -20,7 +20,6 @@ import ListenToPlayer from "@/components/ListenToPlayer" import MainArticle from "@/components/MainArticle" import PageHero from "@/components/PageHero" import { StandaloneQuizWidget } from "@/components/Quiz/QuizWidget" -import { Simulator } from "@/components/Simulator" import { SIMULATOR_ID } from "@/components/Simulator/constants" import Translation from "@/components/Translation" import { ButtonLink } from "@/components/ui/buttons/Button" @@ -30,9 +29,8 @@ import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getMetadata } from "@/lib/utils/metadata" import { getRequiredNamespacesForPage } from "@/lib/utils/translations" -import { buildSimulatorData } from "@/data/WalletSimulatorData" - import WalletsPageJsonLD from "./page-jsonld" +import { WalletSimulator } from "./WalletSimulator" import DappsImage from "@/public/images/doge-computer.png" import ETHImage from "@/public/images/eth-logo.png" @@ -50,7 +48,6 @@ const Page = async (props: { params: Promise }) => { const params = await props.params const { locale } = params const t = await getTranslations("page-wallets") - const simT = await getTranslations("simulator") setRequestLocale(locale) @@ -303,14 +300,14 @@ const Page = async (props: { params: Promise }) => { {locale === "en" ? (
- +

Interactive tutorial

How to use a wallet

-
+
) : ( diff --git a/src/data/WalletSimulatorData.tsx b/src/data/WalletSimulatorData.tsx index 7ccd0951d78..565efce92fc 100644 --- a/src/data/WalletSimulatorData.tsx +++ b/src/data/WalletSimulatorData.tsx @@ -26,25 +26,8 @@ import { import { CONTACTS } from "../components/Simulator/screens/SendReceive/constants" import type { SimulatorData } from "../components/Simulator/types" -/** - * The translate function shape shared by both useTranslations (client) - * and getTranslations (server) from next-intl. - */ -type TranslateFn = ReturnType> - -/** - * Hook returning translated simulator data for client components. - */ export function useWalletOnboardingSimData(): SimulatorData { const t = useTranslations("simulator") - return buildSimulatorData(t) -} - -/** - * Build simulator data with the provided translate function. - * Works with both useTranslations (client) and getTranslations (server). - */ -export function buildSimulatorData(t: TranslateFn): SimulatorData { return { [CREATE_ACCOUNT]: { title: t("sim-ca-title"), From 4361020588799644ddd79f8cd1f007a6b4179e58 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 17:33:38 +0200 Subject: [PATCH 020/109] revert spanish translations --- src/intl/es/page-index.json | 245 +++++++++++++++++++----------------- 1 file changed, 132 insertions(+), 113 deletions(-) diff --git a/src/intl/es/page-index.json b/src/intl/es/page-index.json index 0bfcb944c83..eed909b1cda 100644 --- a/src/intl/es/page-index.json +++ b/src/intl/es/page-index.json @@ -1,121 +1,140 @@ { - "page-index-meta-title": "Ethereum.org: La guía completa sobre Ethereum", - "page-index-meta-description": "Ethereum es una plataforma mundial descentralizada para el dinero y nuevos tipos de aplicaciones. En Ethereum, se pueden escribir códigos que controlan el dinero y construir aplicaciones accesibles desde cualquier rincón del mundo.", + "page-index-activity-description": "Ethereum es la plataforma líder para emitir, gestionar y liquidar activos digitales. Desde dinero tokenizado e instrumentos financieros hasta activos del mundo real y mercados emergentes, Ethereum proporciona una base segura y neutral para la economía digital.", + "page-index-activity-subtitle": "Actividad en la red principal de Ethereum y en las redes de capa 2", + "page-index-activity-tag": "Actividad", + "page-index-activity-header": "El ecosistema más resistente", + "page-index-activity-action": "Más recursos del ecosistema", + "page-index-activity-action-primary": "Ethereum para instituciones", + "page-index-bento-header": "Una nueva forma de utilizar Internet", + "page-index-bento-assets-action": "Más información acerca de NFT", + "page-index-bento-assets-content": "Desde el arte hasta los bienes raíces y las acciones, cualquier activo puede ser tokenizado en Ethereum para probar y verificar la propiedad digitalmente. Compre, venda, intercambie y cree activos y coleccionables en cualquier momento y en cualquier lugar.", + "page-index-bento-assets-title": "El Internet de los activos", + "page-index-bento-dapps-action": "Explorar apps", + "page-index-bento-dapps-content": "Las aplicaciones creadas en Ethereum funcionan sin vender sus datos. Desde redes sociales hasta juegos o trabajo, utilice la misma cuenta para cada aplicación innovadora mientras mantiene la privacidad y el acceso.", + "page-index-bento-dapps-title": "Aplicaciones que respetan su privacidad", + "page-index-bento-defi-action": "Explorar DeFi", + "page-index-bento-defi-content": "Pida prestado, preste, gane intereses y mucho más, sin necesidad de una cuenta bancaria. El sistema financiero descentralizado de Ethereum está abierto 24/7 para cualquier persona con conexión a internet.", + "page-index-bento-defi-title": "Un sistema financiero abierto a todos", + "page-index-bento-networks-action": "Descubra las capas 2", + "page-index-bento-networks-content": "Cientos de redes de capa 2 están construidas sobre Ethereum. Disfrute de comisiones bajas y transacciones casi instantáneas mientras se beneficia de la seguridad probada de Ethereum.", + "page-index-bento-networks-title": "La red de redes", + "page-index-bento-stablecoins-action": "Descubra las monedas estables", + "page-index-bento-stablecoins-content": "Las stablecoins son monedas que mantienen un precio estable, equiparado a activos estables como el dólar estadounidense. Acceda a pagos globales al instante o almacene valor en dólares digitales en Ethereum.", + "page-index-bento-stablecoins-title": "Dinero digital para el uso diario", + "page-index-builders-action-primary": "Portal para desarrolladores", + "page-index-builders-action-secondary": "Documentación", + "page-index-builders-description": "Ethereum alberga el ecosistema de desarrolladores más grande y dinámico de la Web3. Use JavaScript y Python, o aprenda un lenguaje de contratos inteligentes como Solidity o Vyper para crear su propia aplicación.", + "page-index-builders-tag": "Creadores", + "page-index-builders-header": "La comunidad de creadores de cadena de bloques más grande del mundo", + "page-index-calendar-add": "Añadir al calendario", + "page-index-calendar-fallback": "No hay próximas reuniones", + "page-index-calendar-title": "Próximas llamadas", + "page-index-community-action": "Más de ethereum.org", + "page-index-community-description-1": "Cientos de traductores, codificadores, diseñadores, redactores y miembros entusiastas de la comunidad crean y mantienen el sitio web ethereum.org cada mes.", + "page-index-community-description-2": "Haga preguntas, conéctese con personas de todo el mundo y contribuya al sitio web. ¡Obtendrá experiencia práctica relevante y recibirá orientación durante el proceso!", + "page-index-community-description-3": "La comunidad de Ethereum.org es el lugar ideal para comenzar y aprender.", + "page-index-community-tag": "Comunidad de Ethereum.org", + "page-index-community-header": "Creada por la comunidad", + "page-index-cta-dapps-description": "Finanzas, juegos e interacción social", + "page-index-cta-dapps-label": "Pruebe apps", + "page-index-cta-get-eth-description": "La moneda de Ethereum", + "page-index-cta-get-eth-label": "Consiga ETH", + "page-index-cta-wallet-description": "Cree cuentas y administre activos", + "page-index-cta-wallet-label": "Seleccione una billetera", + "page-index-cta-build-apps-description": "Cree su primera aplicación", + "page-index-cta-build-apps-label": "Empezar a crear", "page-index-description": "La plataforma líder para aplicaciones innovadoras y redes de cadena de bloques", - "page-index-title": "Bienvenidos a Ethereum", + "page-index-developers-code-example-description-0": "Construya un banco impulsado por la lógica que programe", + "page-index-developers-code-example-description-1": "Cree tókenes que pueda transferir y usar en distintas aplicaciones", + "page-index-developers-code-example-description-2": "Utilice lenguajes existentes para interactuar con Ethereum y otras aplicaciones", + "page-index-developers-code-example-description-3": "Reimagine los servicios existentes como aplicaciones abiertas y descentralizadas", + "page-index-developers-code-example-title-0": "Su propio banco", + "page-index-developers-code-example-title-1": "Su propia moneda", + "page-index-developers-code-example-title-2": "Una cartera Ethereum en JavaScript", + "page-index-developers-code-example-title-3": "Un DNS abierto y sin permisos", + "page-index-developers-code-examples": "Ejemplos de código", + "page-index-events-action": "Ver todos los eventos", + "page-index-events-header": "Eventos de Ethereum", + "page-index-events-subtitle": "Las comunidades de Ethereum organizan eventos en todo el mundo, durante todo el año", "page-index-hero-image-alt": "Una ilustración de una ciudad futurista, que representa el ecosistema Ethereum.", - "page-index-hero-title": "El internet que te pertenece", - "page-index-hero-subtitle": "Ethereum es la red global donde controlas tus activos, tus datos y tu identidad.", - "page-index-hero-cta": "Empieza aquí", - "page-index-cta-wallet-label": "Elige una billetera", - "page-index-cta-wallet-description": "Crea cuentas y administra activos", - "page-index-cta-get-eth-label": "Consigue ETH", - "page-index-cta-get-eth-description": "La moneda de Ethereum", - "page-index-cta-dapps-label": "Prueba apps", - "page-index-cta-dapps-description": "Descubre lo que Ethereum puede hacer", - "page-index-cta-build-apps-label": "Empieza a crear", - "page-index-cta-build-apps-description": "Crea tu primera aplicación", - "page-index-cta-learn-label": "Aprende Ethereum", - "page-index-modal-title": "¿Qué te trae por aquí?", - "page-index-modal-description": "Elige tu camino: recursos para principiantes, desarrolladores o empresas.", - "page-index-modal-beginners": "Para principiantes", - "page-index-modal-explorers": "Para exploradores", - "page-index-modal-builders": "Para desarrolladores", - "page-index-modal-enterprise": "Para empresas", - "page-index-modal-what-is-ethereum": "¿Qué es Ethereum?", - "page-index-modal-pick-wallet": "Elige una billetera", - "page-index-modal-get-eth": "Consigue ETH", - "page-index-modal-try-apps": "Prueba apps", - "page-index-modal-start-building": "Empieza a crear", - "page-index-modal-docs": "Documentación", - "page-index-modal-founders": "Fundadores", - "page-index-modal-institutions": "Instituciones", - "page-index-kpi-tag": "El internet de los usuarios", - "page-index-kpi-title": "Ethereum te devuelve el control de tus activos", - "page-index-kpi-description": "Tu cuenta bancaria es un registro en la base de datos de otra persona. Tu aplicación es un archivo en el servidor de otra persona. Ethereum es una red alternativa donde posees tus activos directamente.", - "page-index-kpi-holders": "Poseedores de ETH", - "page-index-kpi-transactions": "Transacciones hoy", - "page-index-carousel-privacy-tag": "TU PRIVACIDAD ES TUYA", - "page-index-carousel-privacy-title": "Usa internet sin ser vigilado", - "page-index-carousel-privacy-subtitle": "La mayoría de las apps rastrean lo que haces, con quién hablas y lo que posees. Venden esos datos o los entregan cuando se los piden. En Ethereum, tu actividad puede permanecer privada.", - "page-index-carousel-privacy-description": "Sin cuentas vinculadas a tu nombre. Sin empresas vigilando tu saldo.", - "page-index-carousel-privacy-cta": "Usa apps que preservan tu privacidad →", - "page-index-carousel-privacy-traditional-label": "APPS TRADICIONALES", - "page-index-carousel-privacy-traditional-value": "Tus datos son su producto", - "page-index-carousel-privacy-ethereum-label": "APPS DE ETHEREUM", - "page-index-carousel-privacy-ethereum-value": "Privado por defecto", - "page-index-carousel-remittances-tag": "PAGOS INTERNACIONALES", - "page-index-carousel-remittances-title": "Envía dinero a casa en {minutes} minutos", - "page-index-carousel-remittances-subtitle": "Evita la comisión de {wireFee} y la espera de {days}+ días.", - "page-index-carousel-remittances-description": "Envía stablecoins a cualquier persona, en cualquier parte del mundo, por solo {txFee}. Los fondos llegan casi al instante.", - "page-index-carousel-remittances-cta": "Pruébalo tú mismo →", - "page-index-carousel-remittances-traditional-label": "TRANSFERENCIA BANCARIA", - "page-index-carousel-remittances-traditional-value": "{min}-{max} días", - "page-index-carousel-remittances-ethereum-label": "ETHEREUM", - "page-index-carousel-remittances-ethereum-value": "{minutes} minutos", - "page-index-carousel-borrowing-tag": "ACCESO FINANCIERO", - "page-index-carousel-borrowing-title": "Pide prestado sin historial crediticio", - "page-index-carousel-borrowing-subtitle": "No necesitas un puntaje de crédito para empezar.", - "page-index-carousel-borrowing-description": "Con apps DeFi en Ethereum, puedes aportar colateral y acceder a crédito al instante, sin necesidad de permisos.", - "page-index-carousel-borrowing-cta": "Más información sobre DeFi →", - "page-index-carousel-borrowing-traditional-label": "BANCO TRADICIONAL", - "page-index-carousel-borrowing-traditional-value": "Verificación crediticia", - "page-index-carousel-borrowing-ethereum-label": "EN ETHEREUM", - "page-index-carousel-borrowing-ethereum-value": "Basado en colateral", - "page-index-trust-image-alt": "Ilustración de la comunidad Ethereum", - "page-index-trust-never-offline": "Nunca fuera de línea", - "page-index-trust-uptime": "{uptime} de disponibilidad", - "page-index-trust-years": "{count} años", - "page-index-trust-since": "Desde 2015", - "page-index-trust-tag": "Trayectoria comprobada", - "page-index-trust-title": "Construido para durar", - "page-index-trust-description-1": "Ethereum ha funcionado continuamente desde 2015 sin un solo segundo de inactividad.", - "page-index-trust-description-2": "El código es abierto para que cualquiera lo verifique. Ninguna empresa lo controla, nadie puede apagarlo, y miles de operadores independientes lo mantienen en funcionamiento en todo el mundo.", - "page-index-trust-cta": "Consigue ETH", - "page-index-simulator-tag": "Gratis para siempre", - "page-index-simulator-title": "Prueba Ethereum en tu navegador", - "page-index-simulator-subtitle": "Experimenta cómo funciona Ethereum. Solo haz clic y explora.", - "page-index-features-title": "Lo que hace a Ethereum", - "page-index-features-title-highlight": "diferente", - "page-index-features-subtitle": "Principios que distinguen a Ethereum de los sistemas tradicionales", - "page-index-features-ownership-title": "Propiedad directa", - "page-index-features-ownership-description-1": "Tu saldo bancario es una ", - "page-index-features-ownership-description-custody": "promesa de custodia", - "page-index-features-ownership-description-2": "Tu saldo en Ethereum es propiedad real.", - "page-index-features-ownership-stat": "{volume}+", - "page-index-features-ownership-stat-label": "Volumen diario de transacciones", - "page-index-features-public-rules-title": "Reglas públicas", - "page-index-features-public-rules-description": "El código es público, los acuerdos se ejecutan exactamente como están escritos. Piensa en una máquina expendedora versus esperar que el cajero te dé el cambio correcto.", - "page-index-features-global-title": "Global", - "page-index-features-global-description": "Cualquier persona, en cualquier lugar, puede usar Ethereum. No se necesita permiso.", - "page-index-features-free-access-title": "Acceso libre", - "page-index-features-free-access-description": "Sin verificación de crédito, sin saldo mínimo, sin aprobación de cuenta. Si tienes internet, ya estás dentro.", - "page-index-features-nobody-owns-title": "Nadie es dueño de Ethereum", - "page-index-features-nobody-owns-description": "Los cambios ocurren a través de propuestas abiertas en las que cualquiera puede participar. Piensa en un huerto comunitario versus una granja corporativa.", - "page-index-features-cta": "¿Qué es Ethereum?", - "page-index-get-started-title": "Empieza en Ethereum", - "page-index-get-started-subtitle": "Toma {minutes} minutos para empezar. Sin verificación de crédito, sin papeleo, sin saldo mínimo.", - "page-index-get-started-learn-title": "Entiende Ethereum", - "page-index-get-started-learn-description": "Empieza aquí. Aprende qué es, por qué importa y cómo funciona en un lenguaje sencillo.", - "page-index-get-started-learn-bullet-1": "¿Qué es Ethereum?", - "page-index-get-started-learn-bullet-2": "¿Cómo funcionan las billeteras?", - "page-index-get-started-learn-bullet-3": "DeFi, stablecoins y NFTs explicados", - "page-index-get-started-learn-cta": "Empieza a aprender", - "page-index-get-started-build-title": "Empieza a crear", - "page-index-get-started-build-description": "Para desarrolladores. Accede a documentación, herramientas y tutoriales para crear en Ethereum.", - "page-index-get-started-build-bullet-1": "Documentación para desarrolladores", - "page-index-get-started-build-bullet-2": "Tutoriales de contratos inteligentes", - "page-index-get-started-build-bullet-3": "Herramientas y frameworks de desarrollo", - "page-index-get-started-build-cta": "Ver materiales", - "page-index-get-started-enterprise-title": "Para empresas", - "page-index-get-started-enterprise-description": "Casos de uso empresariales, recursos institucionales y cómo Ethereum puede servir a tu organización.", - "page-index-get-started-enterprise-bullet-1": "Casos de uso empresariales", - "page-index-get-started-enterprise-bullet-2": "Redes privadas y con permisos", - "page-index-get-started-enterprise-bullet-3": "Recursos institucionales", - "page-index-get-started-enterprise-cta": "Explorar para empresas", + "page-index-join-action-contribute-description": "Descubra todas las diferentes formas en que puede ayudar a que ethereum.org crezca y mejore.", + "page-index-join-action-contribute-label": "Cómo contribuir", + "page-index-join-action-discord-description": "Para hacer preguntas, coordinar contribuciones y unirse a las llamadas de la comunidad.", + "page-index-join-action-github-description": "Contribuya en el código, diseño, artículos, etc.", + "page-index-join-action-twitter-description": "Para mantenerse al día con nuestras actualizaciones y noticias importantes.", + "page-index-join-description": "El sitio web ethereum.org está construido y mantenido por miles de traductores, programadores, diseñadores, redactores y miembros de la comunidad. Puede proponer ediciones a cualquiera de los contenidos de este sitio de código abierto.", + "page-index-join-header": "Únase a ethereum.org", + "page-index-join-action-hub": "Centro de colaboradores de ethereum.org", + "page-index-learn-description": "Ethereum es una red blockchain descentralizada y una plataforma de desarrollo de software, impulsada por la criptomoneda ether (ETH). Estos recursos son su puerta de entrada para navegar, comprender y utilizar Ethereum con confianza.", + "page-index-what-is-ethereum-title": "¿Qué es Ethereum?", + "page-index-what-is-ethereum-description-1": "Ethereum es una red blockchain de código abierto descentralizada y una plataforma de desarrollo de software, impulsada por la criptomoneda ether (ETH). Ethereum es la base segura y global para una nueva generación de aplicaciones imparables.", + "page-index-what-is-ethereum-description-2": "La red de Ethereum está abierta a todo el mundo: no se necesita ningún permiso. No tiene propietario y está construida y mantenida por miles de personas, organizaciones y usuarios de todo el mundo.", + "page-index-what-is-ethereum-action": "Obtener información sobre Ethereum", + "page-index-what-is-ether-title": "¿Qué es el ETH?", + "page-index-what-is-ether-description-1": "Ether (ETH) es la criptomoneda nativa que impulsa la red de Ethereum, utilizada para pagar las comisiones de transacción y asegurar la blockchain mediante el staking.", + "page-index-what-is-ether-description-2": "Más allá de su función técnica, ETH es dinero digital abierto y programable. Se utiliza para pagos globales, como garantía para préstamos y como reserva de valor que no depende de ninguna entidad central.", + "page-index-what-is-ether-action": "Obtenga más información sobre el ether", + "page-index-learn-tag": "Aprender", + "page-index-network-tag": "Red", + "page-index-token-tag": "Token", + "page-index-learn-header": "Comprenda Ethereum", + "page-index-meta-description": "Ethereum es una plataforma mundial descentralizada para el dinero y nuevos tipos de aplicaciones. En Ethereum, se pueden escribir códigos que controlan el dinero y construir aplicaciones accesibles desde cualquier rincón del mundo.", + "page-index-meta-title": "Ethereum.org: La guía completa sobre Ethereum", + "page-index-network-stats-total-eth-staked": "Valor que protege Ethereum", + "page-index-network-stats-tx-cost-description": "Costo de transacción promedio", + "page-index-network-stats-tx-day-description": "Transacciones en las últimas 24 h", + "page-index-network-stats-value-defi-description": "Valor bloqueado en DeFi", + "page-index-network-stats-total-value-held": "Valor total mantenido en Ethereum", + "page-index-popular-topics-ethereum": "¿Qué es Ethereum?", + "page-index-popular-topics-header": "Temas populares", + "page-index-popular-topics-action": "Más guías en el Centro de aprendizaje de Ethereum", + "page-index-popular-topics-roadmap": "Hoja de ruta de Ethereum", + "page-index-popular-topics-start": "Guías paso a paso de Ethereum", + "page-index-popular-topics-wallets": "¿Qué son las carteras de criptomonedas?", + "page-index-popular-topics-whitepaper": "Informe oficial de Ethereum", + "page-index-posts-action": "Leer más sobre estos sitios web", + "page-index-posts-header": "Noticias de Ethereum", + "page-index-posts-subtitle": "Las últimas publicaciones de blog y actualizaciones de la comunidad", + "page-index-title": "Bienvenidos a Ethereum", + "page-index-use-cases-tag": "Casos de uso", + "page-index-values-description": "Sea parte de la revolución digital", + "page-index-values-header": "Internet está cambiando", + "page-index-values-legacy": "Legado", + "page-index-values-tag": "Valores", + "page-index-values-ownership-legacy-label": "Propiedad restringida", + "page-index-values-ownership-legacy-content-0": "En un banco o una plataforma de redes sociales tradicionales, la organización administra sus activos y datos, y usted confía en ellos para acceder a estos y controlarlos.", + "page-index-values-ownership-legacy-content-1": "Es posible que utilicen sus datos de maneras con las que usted no esté de acuerdo, según sus políticas.", + "page-index-values-ownership-ethereum-label": "Propiedad directa", + "page-index-values-ownership-ethereum-content-0": "Con Ethereum, solo usted tiene acceso y control. Nadie más debería poder usar sus activos. Puede decidir a quién conceder ese permiso.", + "page-index-values-fairness-legacy-label": "Sistema discriminatorio", + "page-index-values-fairness-legacy-content-0": "Hoy en día, no todo el mundo tiene el mismo acceso a los servicios financieros. Algunas personas pueden enfrentarse a barreras de acceso debido a su ubicación o nacionalidad.", + "page-index-values-fairness-ethereum-label": "Acceso igualitario", + "page-index-values-fairness-ethereum-content-0": "Creemos que todo el mundo debería poder beneficiarse de un sistema global. Por eso, Ethereum otorga acceso igualitario a todos en todo el mundo, independientemente de quién sea o de dónde venga.", + "page-index-values-privacy-legacy-label": "Sin privacidad", + "page-index-values-privacy-legacy-content-0": "No podemos esperar que los gobiernos, corporaciones u otras grandes organizaciones sin rostro nos concedan privacidad por su beneficencia.", + "page-index-values-privacy-legacy-content-1": "La mayoría de las aplicaciones recopilan la mayor cantidad posible de información personal para poder enviarle marketing personalizado.", + "page-index-values-privacy-ethereum-label": "Privacidad orientada", + "page-index-values-privacy-ethereum-content-0": "La comunidad de Ethereum respeta la privacidad. Tiene derecho a usar aplicaciones sin revelar su identidad ni proporcionar sus datos de contacto.", + "page-index-values-integration-legacy-label": "Fragmentación", + "page-index-values-integration-legacy-content-0": "La mayoría de las aplicaciones lo presionan para crear cuentas separadas, lo que hace que sea difícil recordar todos sus datos de inicio de sesión y registros.", + "page-index-values-integration-ethereum-label": "Integración", + "page-index-values-integration-ethereum-content-0": "Con Ethereum puede reutilizar una cuenta en todas las aplicaciones. No es necesario registrarse individualmente.", + "page-index-values-decentralization-legacy-label": "Centralizado", + "page-index-values-decentralization-legacy-content-0": "Las empresas son propiedad de empresarios y accionistas privados. Solo ellos ejercen el control sobre la empresa y son los que más se benefician de su éxito.", + "page-index-values-decentralization-ethereum-label": "Sistema descentralizado", + "page-index-values-decentralization-ethereum-content-0": "Al igual que Internet, Ethereum no pertenece a nadie. Es un recurso compartido que todos pueden controlar de forma equitativa. No existe un único propietario que pueda controlarlo.", + "page-index-values-censorship-legacy-label": "Censurable", + "page-index-values-censorship-legacy-content-0": "Las plataformas modernas y sus reglas cambian con frecuencia. Pueden verse influidas por las partes interesadas, la dirección de la empresa o incluso por regímenes opresivos.", + "page-index-values-censorship-ethereum-label": "Resistente a la censura", + "page-index-values-censorship-ethereum-content-0": "La resistencia a la opresión es el principio fundamental de Ethereum. Su funcionalidad debe ser siempre justa e imparcial.", + "page-index-values-censorship-ethereum-content-1": "Ethereum no puede ser controlado por ningún estado nacional, empresa ni individuo.", + "page-index-values-open-legacy-label": "Cerrado a la mayoría", + "page-index-values-open-legacy-content-0": "Las empresas protegen su propiedad intelectual y no la comparten. Nadie fuera de la empresa puede ver cómo funcionan las cosas, solucionar problemas o hacer mejoras. Es difícil que las personas creen nuevas herramientas o personalicen las existentes.", + "page-index-values-open-ethereum-label": "Abierto a todos", + "page-index-values-open-ethereum-content-0": "Ethereum es de código abierto. Cualquiera puede ver, usar y mejorar el código, haciéndolo mejor para todos.", "page-index-fusaka-network-upgrade": "Actualización de la red", "page-index-fusaka-description": "Para una red de Ethereum más rápida, segura y fácil de usar |", "page-index-fusaka-read-more": "Más información", "page-index-fusaka-going-live-in": "Se activará

en", "page-index-fusaka-live-now": "En directo" -} +} \ No newline at end of file From 52b2bab712e64cc78440e37b2e73e64c775a34a5 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:07:48 -0700 Subject: [PATCH 021/109] patch: intl-pipeline.yml input description Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- .github/workflows/intl-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/intl-pipeline.yml b/.github/workflows/intl-pipeline.yml index e3c020f3700..0cc961075c7 100644 --- a/.github/workflows/intl-pipeline.yml +++ b/.github/workflows/intl-pipeline.yml @@ -21,7 +21,7 @@ on: default: "dev" type: string target_branch: - description: "Override target branch (default: intl/pending)" + description: "Override target branch (default: intl/pending-{base_branch})" required: false type: string concurrency: From 3bc4d8e7956319c7a40a75ee5f288bb136bfe71f Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:17:46 -0700 Subject: [PATCH 022/109] fix(intl-pipeline): reorganize manifest paths Move manifests from inline with content to dedicated .manifests/ directory. Structure: .manifests/{dest-path}/source.json | translation.json Fixes multi-JSON-namespace collision where one shared manifest per locale dir lost data. Each file now gets its own manifest pair. MANIFESTS_DIR in constants.ts. Co-Authored-By: Claude Opus 4.6 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- next.config.js | 2 + src/scripts/intl-pipeline/constants.ts | 4 ++ src/scripts/intl-pipeline/main.ts | 90 ++++++++++---------------- 3 files changed, 39 insertions(+), 57 deletions(-) diff --git a/next.config.js b/next.config.js index 775890c7dda..b638ae81c86 100644 --- a/next.config.js +++ b/next.config.js @@ -240,6 +240,8 @@ module.exports = (phase) => { "public/content", // Exclude source maps generated by Sentry to reduce function bundle size ".next/server/**/*.map", + // Translation manifests (canonical name in src/scripts/intl-pipeline/constants.ts) + ".manifests", ], }, } diff --git a/src/scripts/intl-pipeline/constants.ts b/src/scripts/intl-pipeline/constants.ts index 5abd60927ef..6a9765b69b5 100644 --- a/src/scripts/intl-pipeline/constants.ts +++ b/src/scripts/intl-pipeline/constants.ts @@ -7,3 +7,7 @@ // Well within Gemini 3.1 Pro's 65K output token limit // Conservative: prefer more calls over larger chunks export const MAX_CHUNK_BYTES = 65_536 + +// Root directory for translation manifests (relative to repo root) +// Structure: {MANIFESTS_DIR}/{dest-file-path}/source.json | translation.json +export const MANIFESTS_DIR = ".manifests" diff --git a/src/scripts/intl-pipeline/main.ts b/src/scripts/intl-pipeline/main.ts index 2edcc9f657a..ae9b6bd93d2 100644 --- a/src/scripts/intl-pipeline/main.ts +++ b/src/scripts/intl-pipeline/main.ts @@ -61,6 +61,7 @@ import { GLOSSARY_API_URL, validateTargetPath, } from "./config" +import { MANIFESTS_DIR } from "./constants" import type { LlmTranslator } from "./pipeline" import { pipeline, PIPELINE_CONFIG } from "./pipeline" @@ -82,19 +83,16 @@ function log(msg: string) { console.log(`[pipeline] ${msg}`) } -function readSourceManifestPath( +/** + * Get manifest path for a given destination file. + * Structure: .manifests/{destPath}/source.json or translation.json + * Example: .manifests/public/content/translations/ar/about/index.md/source.json + */ +function getManifestPath( destPath: string, - fileType: string, - locale: string + type: "source" | "translation" ): string { - if (fileType === "markdown") { - return path.join( - process.cwd(), - path.dirname(destPath), - ".manifest-source.json" - ) - } - return path.join(process.cwd(), `src/intl/${locale}/.manifest-source.json`) + return path.join(process.cwd(), MANIFESTS_DIR, destPath, `${type}.json`) } /** @@ -418,39 +416,22 @@ async function runFullTranslation( ? buildMarkdownManifest(file.content, file.path, baseBranchSha) : buildJsonManifest(file.content, file.path, baseBranchSha) - if (file.type === "markdown") { - const manifestPath = destPath.replace(/index\.md$/, ".manifest-source.json") - await committer.commitFile(manifestPath, sourceManifest, locale) + // Commit source manifest + const smDest = getManifestPath(destPath, "source") + await committer.commitFile(smDest, sourceManifest, locale) - if (result.placeholderOrder && result.placeholderMap) { - const parsed = JSON.parse(sourceManifest) - const tm = buildLocaleTranslationManifest({ - locale, - englishManifestHash: parsed.rootHash, - placeholderOrder: result.placeholderOrder, - placeholderMap: result.placeholderMap, - sections: { - _all: { translatedAt: new Date().toISOString(), status: "success" }, - }, - }) - const tmPath = destPath.replace( - /index\.md$/, - ".manifest-translation.json" - ) - await committer.commitFile(tmPath, tm, locale) - } - } else { - const manifestPath = `src/intl/${locale}/.manifest-source.json` - await committer.commitFile(manifestPath, sourceManifest, locale) - - const placeholderData = - result.placeholderOrder && result.placeholderMap - ? { - placeholderOrder: result.placeholderOrder, - placeholderMap: result.placeholderMap, - } - : extractPlaceholderData(parseEnglishJson(file.content)) + // Commit translation manifest + const placeholderData = + result.placeholderOrder && result.placeholderMap + ? { + placeholderOrder: result.placeholderOrder, + placeholderMap: result.placeholderMap, + } + : file.type === "json" + ? extractPlaceholderData(parseEnglishJson(file.content)) + : null + if (placeholderData) { const parsed = JSON.parse(sourceManifest) const tm = buildLocaleTranslationManifest({ locale, @@ -461,8 +442,8 @@ async function runFullTranslation( _all: { translatedAt: new Date().toISOString(), status: "success" }, }, }) - const jsonTmPath = `src/intl/${locale}/.manifest-translation.json` - await committer.commitFile(jsonTmPath, tm, locale) + const tmDest = getManifestPath(destPath, "translation") + await committer.commitFile(tmDest, tm, locale) } log(`[${locale}] ${destPath}: committed`) @@ -554,13 +535,8 @@ async function runIncremental( ? buildMarkdownManifest(englishB, file.path, baseBranchSha) : buildJsonManifest(englishB, file.path, baseBranchSha) - if (file.type === "markdown") { - const smPath = destPath.replace(/index\.md$/, ".manifest-source.json") - await committer.commitFile(smPath, sourceManifest, locale) - } else { - const smPath = `src/intl/${locale}/.manifest-source.json` - await committer.commitFile(smPath, sourceManifest, locale) - } + const smDest = getManifestPath(destPath, "source") + await committer.commitFile(smDest, sourceManifest, locale) log(`[${locale}] ${destPath}: committed (incremental)`) return { tokens } @@ -620,7 +596,7 @@ async function main() { for (const file of englishFiles) { for (const locale of targetLanguages) { const destPath = getDestinationFromPath(file.path, locale) - const smPath = readSourceManifestPath(destPath, file.type, locale) + const smPath = getManifestPath(destPath, "source") const localePath = readLocalePath( destPath, file.type, @@ -674,11 +650,11 @@ async function main() { file.type === "markdown" ? buildMarkdownManifest(file.content, file.path, baseBranchSha) : buildJsonManifest(file.content, file.path, baseBranchSha) - const manifestDest = - file.type === "markdown" - ? destPath.replace(/index\.md$/, ".manifest-source.json") - : `src/intl/${locale}/.manifest-source.json` - await committer.commitFile(manifestDest, sourceManifest, locale) + await committer.commitFile( + getManifestPath(destPath, "source"), + sourceManifest, + locale + ) }) continue } From 355070dc78bbe2127019935ad5a4b9116ce5b18c Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 19:27:23 +0200 Subject: [PATCH 023/109] perf: move homepage dynamic imports to client wrapper with ssr: false --- app/[locale]/_components/HomepageLazy.tsx | 35 ++++++++ app/[locale]/page.tsx | 79 ++++++++----------- .../Homepage/SimulatorSection/index.tsx | 60 ++++---------- 3 files changed, 80 insertions(+), 94 deletions(-) create mode 100644 app/[locale]/_components/HomepageLazy.tsx diff --git a/app/[locale]/_components/HomepageLazy.tsx b/app/[locale]/_components/HomepageLazy.tsx new file mode 100644 index 00000000000..bb7517e56d7 --- /dev/null +++ b/app/[locale]/_components/HomepageLazy.tsx @@ -0,0 +1,35 @@ +"use client" + +import dynamic from "next/dynamic" + +import { Section } from "@/components/ui/section" + +const SectionSkeleton = ({ className }: { className?: string }) => ( +
+
+
+) + +export const KPISection = dynamic( + () => import("@/components/Homepage/KPISection"), + { + ssr: false, + loading: () => , + } +) + +export const SavingsCarousel = dynamic( + () => import("@/components/Homepage/SavingsCarousel"), + { + ssr: false, + loading: () => , + } +) + +export const SimulatorSection = dynamic( + () => import("@/components/Homepage/SimulatorSection"), + { + ssr: false, + loading: () => , + } +) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index aee24524ca0..5665c09c036 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,6 +1,4 @@ -import { Suspense } from "react" import { pick } from "lodash" -import dynamic from "next/dynamic" import { notFound } from "next/navigation" import { getMessages, @@ -17,31 +15,22 @@ import TrustLogos from "@/components/Homepage/TrustLogos" import I18nProvider from "@/components/I18nProvider" import MainArticle from "@/components/MainArticle" import { TrackedSection } from "@/components/TrackedSection" -import { Section, SectionHeader, SectionTag } from "@/components/ui/section" +import { SectionHeader, SectionTag } from "@/components/ui/section" import { getDirection } from "@/lib/utils/direction" import { getMetadata } from "@/lib/utils/metadata" import { DEFAULT_LOCALE, LOCALES_CODES } from "@/lib/constants" +import { + KPISection, + SavingsCarousel, + SimulatorSection, +} from "./_components/HomepageLazy" import IndexPageJsonLD from "./page-jsonld" import { getAccountHolders, getGrowThePieData } from "@/lib/data" -const KPISection = dynamic(() => import("@/components/Homepage/KPISection")) -const SavingsCarousel = dynamic( - () => import("@/components/Homepage/SavingsCarousel") -) -const SimulatorSection = dynamic( - () => import("@/components/Homepage/SimulatorSection") -) - -const SectionSkeleton = ({ className }: { className?: string }) => ( -
-
-
-) - const Page = async (props: { params: Promise }) => { const params = await props.params const { locale } = params @@ -93,22 +82,18 @@ const Page = async (props: { params: Promise }) => {
- }> - - + - }> - - + @@ -116,24 +101,22 @@ const Page = async (props: { params: Promise }) => { - }> - - - {t("page-index-simulator-tag")} - - - {t("page-index-simulator-title")} - -

- {t("page-index-simulator-subtitle")} -

-
- } - /> - + + + {t("page-index-simulator-tag")} + + + {t("page-index-simulator-title")} + +

+ {t("page-index-simulator-subtitle")} +

+ + } + /> diff --git a/src/components/Homepage/SimulatorSection/index.tsx b/src/components/Homepage/SimulatorSection/index.tsx index b6093f438aa..713d79cd5bd 100644 --- a/src/components/Homepage/SimulatorSection/index.tsx +++ b/src/components/Homepage/SimulatorSection/index.tsx @@ -1,7 +1,6 @@ "use client" -import { useEffect, useState } from "react" -import { useIntersectionObserver } from "usehooks-ts" +import { useState } from "react" import { SEND_RECEIVE } from "@/components/Simulator/constants" import { Explanation } from "@/components/Simulator/Explanation" @@ -19,24 +18,9 @@ type SimulatorSectionProps = { header?: React.ReactNode } -/** - * Loading skeleton that matches simulator phone dimensions - */ -const SimulatorSkeleton = () => ( -
-
-
-) - const SimulatorSection = ({ className, header }: SimulatorSectionProps) => { const walletOnboardingSimData = useWalletOnboardingSimData() const sendReceiveData = walletOnboardingSimData[SEND_RECEIVE] - const { ref: sectionRef, isIntersecting: isVisible } = - useIntersectionObserver({ - rootMargin: "200px", - freezeOnceVisible: true, - }) - const [isLoaded, setIsLoaded] = useState(false) const [step, setStep] = useState(0) const { Screen, explanations, ctaLabels, finalCtaLink } = sendReceiveData @@ -52,39 +36,23 @@ const SimulatorSection = ({ className, header }: SimulatorSectionProps) => { openPath: () => {}, } - useEffect(() => { - if (isVisible) { - const timer = setTimeout(() => setIsLoaded(true), 300) - return () => clearTimeout(timer) - } - }, [isVisible]) - return ( -
+
{header}
- {!isVisible || !isLoaded ? ( -
- -
- ) : ( - - )} +
) From bd3e950fd406153cc597be1d811c277699e13d2e Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 19:31:43 +0200 Subject: [PATCH 024/109] rename simulator intl namespace to component-wallet-simulator --- app/[locale]/page.tsx | 2 +- src/components/Simulator/Explanation.tsx | 2 +- src/components/Simulator/ProgressCta.tsx | 2 +- src/components/Simulator/WalletHome/AddressPill.tsx | 2 +- src/components/Simulator/WalletHome/NFTList.tsx | 2 +- src/components/Simulator/WalletHome/SendReceiveButton.tsx | 2 +- src/components/Simulator/WalletHome/SendReceiveButtons.tsx | 2 +- src/components/Simulator/WalletHome/WalletBalance.tsx | 2 +- src/components/Simulator/WalletHome/index.tsx | 2 +- src/components/Simulator/screens/ConnectWeb3/Browser.tsx | 2 +- src/components/Simulator/screens/ConnectWeb3/Slider.tsx | 2 +- src/components/Simulator/screens/ConnectWeb3/Web3App.tsx | 2 +- src/components/Simulator/screens/ConnectWeb3/index.tsx | 2 +- .../Simulator/screens/CreateAccount/GeneratingKeys.tsx | 2 +- .../Simulator/screens/CreateAccount/InitialWordDisplay.tsx | 2 +- .../Simulator/screens/CreateAccount/InteractiveWordSelector.tsx | 2 +- .../Simulator/screens/CreateAccount/RecoveryPhraseNotice.tsx | 2 +- .../Simulator/screens/CreateAccount/WelcomeScreen.tsx | 2 +- src/components/Simulator/screens/SendReceive/ReceiveEther.tsx | 2 +- src/components/Simulator/screens/SendReceive/ReceivedEther.tsx | 2 +- src/components/Simulator/screens/SendReceive/SendEther.tsx | 2 +- .../Simulator/screens/SendReceive/SendFromContacts.tsx | 2 +- src/components/Simulator/screens/SendReceive/SendSummary.tsx | 2 +- src/components/Simulator/screens/SendReceive/Success.tsx | 2 +- src/data/WalletSimulatorData.tsx | 2 +- src/intl/en/{simulator.json => component-wallet-simulator.json} | 0 26 files changed, 25 insertions(+), 25 deletions(-) rename src/intl/en/{simulator.json => component-wallet-simulator.json} (100%) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 5665c09c036..1969e962655 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -62,7 +62,7 @@ const Page = async (props: { params: Promise }) => { const allMessages = await getMessages() const glossary = allMessages["glossary-tooltip"] as Record const messages = { - ...pick(allMessages, "page-index", "simulator"), + ...pick(allMessages, "page-index", "component-wallet-simulator"), "glossary-tooltip": pick(glossary, [ "nft-term", "nft-definition", diff --git a/src/components/Simulator/Explanation.tsx b/src/components/Simulator/Explanation.tsx index 77af31164d9..860319784f5 100644 --- a/src/components/Simulator/Explanation.tsx +++ b/src/components/Simulator/Explanation.tsx @@ -36,7 +36,7 @@ export const Explanation = ({ openPath, logFinalCta, }: ExplanationProps) => { - const t = useTranslations("simulator") + const t = useTranslations("component-wallet-simulator") const { regressStepper, step, totalSteps } = nav const { header, description } = explanation diff --git a/src/components/Simulator/ProgressCta.tsx b/src/components/Simulator/ProgressCta.tsx index 5280a4de4fe..21bd3f1012a 100644 --- a/src/components/Simulator/ProgressCta.tsx +++ b/src/components/Simulator/ProgressCta.tsx @@ -25,7 +25,7 @@ export const ProgressCta = ({ className, ...flexProps }: ProgressCtaProps) => { - const t = useTranslations("simulator") + const t = useTranslations("component-wallet-simulator") return ( export const AddressPill = ({ ...btnProps }: AddressPillProps) => { - const t = useTranslations("simulator") + const t = useTranslations("component-wallet-simulator") return ( } export const NFTList = ({ nfts, ...flexProps }: NFTListProps) => { - const t = useTranslations("simulator") + const t = useTranslations("component-wallet-simulator") const size = useBreakpointValue({ base: "max-w-20 max-h-20", md: "max-w-24 max-h-24", diff --git a/src/components/Simulator/WalletHome/SendReceiveButton.tsx b/src/components/Simulator/WalletHome/SendReceiveButton.tsx index e051f005bbf..a5b09cbff2f 100644 --- a/src/components/Simulator/WalletHome/SendReceiveButton.tsx +++ b/src/components/Simulator/WalletHome/SendReceiveButton.tsx @@ -26,7 +26,7 @@ export const SendReceiveButton = ({ onClick, isAnimated, }: SendReceiveButtonProps) => { - const t = useTranslations("simulator") + const t = useTranslations("component-wallet-simulator") return ( - - - - - {t("page-index-modal-title")} - - - {t("page-index-modal-description")} - - -
- {categories.map((category) => ( - - ))} -
-
- + <> + + + + + + + + {t("page-index-modal-title")} + + + {t("page-index-modal-description")} + + +
+ {categories.map((category) => ( + + ))} +
+
+
+ + {/* Static links for SEO — these URLs are only reachable via the JS + dialog modal above, so we render them visually-hidden to ensure + crawlers can discover them. */} + + ) } From 1e2713c9946227fae661524623a7407c1b451279 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 20:03:50 +0200 Subject: [PATCH 029/109] use design tokens for get-started icon backgrounds instead of hardcoded hex --- src/components/Homepage/GetStartedGrid.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx index aadc82251c2..1c2d21a34ae 100644 --- a/src/components/Homepage/GetStartedGrid.tsx +++ b/src/components/Homepage/GetStartedGrid.tsx @@ -33,7 +33,7 @@ const GetStartedGrid = async ({ { id: "learn", icon: Book, - iconBg: "bg-[#f7ecff] dark:bg-[#2d1a4e]", + iconBg: "bg-purple-50 dark:bg-purple-900", iconColor: "text-primary", title: t("page-index-get-started-learn-title"), description: t("page-index-get-started-learn-description"), @@ -50,7 +50,7 @@ const GetStartedGrid = async ({ { id: "developers", icon: Code, - iconBg: "bg-[#e9f4ff] dark:bg-[#1a2a3e]", + iconBg: "bg-blue-100 dark:bg-blue-900", iconColor: "text-accent-a", title: t("page-index-get-started-build-title"), description: t("page-index-get-started-build-description"), @@ -67,7 +67,7 @@ const GetStartedGrid = async ({ { id: "enterprise", icon: Building2, - iconBg: "bg-[#e6f7f6] dark:bg-[#1a3332]", + iconBg: "bg-teal-100 dark:bg-teal-900", iconColor: "text-accent-c", title: t("page-index-get-started-enterprise-title"), description: t("page-index-get-started-enterprise-description"), From 7af2e231a429317ff65b860ffc86a06eb740701c Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 16 Apr 2026 20:07:14 +0200 Subject: [PATCH 030/109] fix useEthPrice to use internal api route instead of calling coingecko directly --- src/hooks/useEthPrice.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/hooks/useEthPrice.tsx b/src/hooks/useEthPrice.tsx index 4bd3990bc3e..c57ce29052a 100644 --- a/src/hooks/useEthPrice.tsx +++ b/src/hooks/useEthPrice.tsx @@ -5,14 +5,11 @@ export const useEthPrice = (): number => { useEffect(() => { ;(async () => { try { - const data: { ethereum: { usd: number } } = await fetch( - "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" - ).then((res) => res.json()) - const { - ethereum: { usd }, - } = data - if (!usd) throw new Error("Unable to fetch ETH price from CoinGecko") - setEthPrice(usd) + const res = await fetch("/api/gas-eth-price") + if (!res.ok) throw new Error("Failed to fetch ETH price") + const data: { ethPriceUSD: number } = await res.json() + if (!data.ethPriceUSD) throw new Error("Unable to fetch ETH price") + setEthPrice(data.ethPriceUSD) } catch (error) { console.error(error) } From bb02749bfaa317eb3c3b86c157122b416c28e303 Mon Sep 17 00:00:00 2001 From: Melissa Nelson Date: Thu, 16 Apr 2026 14:14:34 -0400 Subject: [PATCH 031/109] update to ensure wallet link is seen --- src/components/Homepage/PersonaModalCTA.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Homepage/PersonaModalCTA.tsx b/src/components/Homepage/PersonaModalCTA.tsx index 730435acbe9..1720a27f5e9 100644 --- a/src/components/Homepage/PersonaModalCTA.tsx +++ b/src/components/Homepage/PersonaModalCTA.tsx @@ -196,10 +196,7 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => { // elsewhere on the page) so crawlers can discover them without JS. const crawlerOnlyLinks = categories.flatMap((cat) => cat.links.filter( - ({ href }) => - !["/what-is-ethereum/", "/wallets/find-wallet/", "/get-eth/"].includes( - href - ) + ({ href }) => !["/what-is-ethereum/", "/get-eth/"].includes(href) ) ) From a8761e3526c27690e3e14b63e55cf47d57462b79 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:22:03 -0700 Subject: [PATCH 032/109] test: add fixture-2 JSON + no-drift tests Second JSON namespace (4 keys, no mutations between english-a and english-b). Tests no-drift detection and manifest path isolation for multiple JSON files. 811 tests passing. Co-Authored-By: Claude Opus 4.6 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- .../incremental/english-a/fixture-2.json | 6 ++++ .../incremental/english-b/fixture-2.json | 6 ++++ .../incremental/locale-a/es/fixture-2.json | 6 ++++ .../incremental/locale-a/ko/fixture-2.json | 6 ++++ .../incremental/locale-a/ur/fixture-2.json | 6 ++++ .../incremental/locale-b/es/fixture-2.json | 6 ++++ .../incremental/locale-b/ko/fixture-2.json | 6 ++++ .../incremental/locale-b/ur/fixture-2.json | 6 ++++ .../incremental-pipeline.spec.ts | 28 +++++++++++++++++++ 9 files changed, 76 insertions(+) create mode 100644 tests/fixtures/incremental/english-a/fixture-2.json create mode 100644 tests/fixtures/incremental/english-b/fixture-2.json create mode 100644 tests/fixtures/incremental/locale-a/es/fixture-2.json create mode 100644 tests/fixtures/incremental/locale-a/ko/fixture-2.json create mode 100644 tests/fixtures/incremental/locale-a/ur/fixture-2.json create mode 100644 tests/fixtures/incremental/locale-b/es/fixture-2.json create mode 100644 tests/fixtures/incremental/locale-b/ko/fixture-2.json create mode 100644 tests/fixtures/incremental/locale-b/ur/fixture-2.json diff --git a/tests/fixtures/incremental/english-a/fixture-2.json b/tests/fixtures/incremental/english-a/fixture-2.json new file mode 100644 index 00000000000..e212e4e39b5 --- /dev/null +++ b/tests/fixtures/incremental/english-a/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "Home", + "nav-about": "About", + "nav-docs": "Documentation", + "nav-community": "Community" +} diff --git a/tests/fixtures/incremental/english-b/fixture-2.json b/tests/fixtures/incremental/english-b/fixture-2.json new file mode 100644 index 00000000000..e212e4e39b5 --- /dev/null +++ b/tests/fixtures/incremental/english-b/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "Home", + "nav-about": "About", + "nav-docs": "Documentation", + "nav-community": "Community" +} diff --git a/tests/fixtures/incremental/locale-a/es/fixture-2.json b/tests/fixtures/incremental/locale-a/es/fixture-2.json new file mode 100644 index 00000000000..9e99b56d1f1 --- /dev/null +++ b/tests/fixtures/incremental/locale-a/es/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "Inicio", + "nav-about": "Acerca de", + "nav-docs": "Documentacion", + "nav-community": "Comunidad" +} diff --git a/tests/fixtures/incremental/locale-a/ko/fixture-2.json b/tests/fixtures/incremental/locale-a/ko/fixture-2.json new file mode 100644 index 00000000000..fd673bed8a9 --- /dev/null +++ b/tests/fixtures/incremental/locale-a/ko/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "홈", + "nav-about": "소개", + "nav-docs": "문서", + "nav-community": "커뮤니티" +} diff --git a/tests/fixtures/incremental/locale-a/ur/fixture-2.json b/tests/fixtures/incremental/locale-a/ur/fixture-2.json new file mode 100644 index 00000000000..c83a8e248d6 --- /dev/null +++ b/tests/fixtures/incremental/locale-a/ur/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "ہوم", + "nav-about": "تعارف", + "nav-docs": "دستاویزات", + "nav-community": "کمیونٹی" +} diff --git a/tests/fixtures/incremental/locale-b/es/fixture-2.json b/tests/fixtures/incremental/locale-b/es/fixture-2.json new file mode 100644 index 00000000000..9e99b56d1f1 --- /dev/null +++ b/tests/fixtures/incremental/locale-b/es/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "Inicio", + "nav-about": "Acerca de", + "nav-docs": "Documentacion", + "nav-community": "Comunidad" +} diff --git a/tests/fixtures/incremental/locale-b/ko/fixture-2.json b/tests/fixtures/incremental/locale-b/ko/fixture-2.json new file mode 100644 index 00000000000..fd673bed8a9 --- /dev/null +++ b/tests/fixtures/incremental/locale-b/ko/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "홈", + "nav-about": "소개", + "nav-docs": "문서", + "nav-community": "커뮤니티" +} diff --git a/tests/fixtures/incremental/locale-b/ur/fixture-2.json b/tests/fixtures/incremental/locale-b/ur/fixture-2.json new file mode 100644 index 00000000000..c83a8e248d6 --- /dev/null +++ b/tests/fixtures/incremental/locale-b/ur/fixture-2.json @@ -0,0 +1,6 @@ +{ + "nav-home": "ہوم", + "nav-about": "تعارف", + "nav-docs": "دستاویزات", + "nav-community": "کمیونٹی" +} diff --git a/tests/unit/intl-pipeline/incremental-pipeline.spec.ts b/tests/unit/intl-pipeline/incremental-pipeline.spec.ts index 46f54b92803..48283145371 100644 --- a/tests/unit/intl-pipeline/incremental-pipeline.spec.ts +++ b/tests/unit/intl-pipeline/incremental-pipeline.spec.ts @@ -36,9 +36,12 @@ const EN_A_MD = read("english-a/fixture-1.md") const EN_B_MD = read("english-b/fixture-1.md") const EN_A_JSON = read("english-a/fixture-1.json") const EN_B_JSON = read("english-b/fixture-1.json") +const EN_A_JSON2 = read("english-a/fixture-2.json") +const EN_B_JSON2 = read("english-b/fixture-2.json") const locA = (lang: string, ext: string) => read(`locale-a/${lang}/fixture-1.${ext}`) +const locA2 = (lang: string) => read(`locale-a/${lang}/fixture-2.json`) const locB = (lang: string, ext: string) => read(`locale-b/${lang}/fixture-1.${ext}`) const locExpected = (lang: string, ext: string) => @@ -635,3 +638,28 @@ for (const lang of LANGS) { expect(result).toBe(expected) }) } + +// =================================================================== +// Fixture-2: No-drift JSON (identical english-a and english-b) +// =================================================================== + +for (const lang of LANGS) { + test(`No-drift [${lang}] JSON fixture-2: output matches locale-A exactly`, () => { + const result = pipeline(EN_A_JSON2, EN_B_JSON2, locA2(lang), "json") + // No drift = output should be byte-for-byte identical to locale-A + expect(result).toBe(locA2(lang)) + }) +} + +// =================================================================== +// Manifest path: distinct paths for multiple JSON files +// =================================================================== + +test("manifest paths are distinct for different JSON files in same locale", () => { + // Import the helper indirectly by testing the path pattern + const path1 = `.manifests/src/intl/ko/fixture-1.json/source.json` + const path2 = `.manifests/src/intl/ko/fixture-2.json/source.json` + expect(path1).not.toBe(path2) + expect(path1).toContain("fixture-1.json/source.json") + expect(path2).toContain("fixture-2.json/source.json") +}) From 7e11a154badf2991a6b03ccd78517f7ac9b86cdc Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:27:13 -0700 Subject: [PATCH 033/109] fix(intl-pipeline): use relative manifest paths for GH API getManifestPath returns relative path (no process.cwd). Absolute path used only for local fs reads. GitHub tree API rejects paths starting with /. Co-Authored-By: Claude Opus 4.6 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- src/scripts/intl-pipeline/main.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/scripts/intl-pipeline/main.ts b/src/scripts/intl-pipeline/main.ts index ae9b6bd93d2..651056c0bb7 100644 --- a/src/scripts/intl-pipeline/main.ts +++ b/src/scripts/intl-pipeline/main.ts @@ -84,7 +84,7 @@ function log(msg: string) { } /** - * Get manifest path for a given destination file. + * Get manifest path (relative to repo root) for a given destination file. * Structure: .manifests/{destPath}/source.json or translation.json * Example: .manifests/public/content/translations/ar/about/index.md/source.json */ @@ -92,7 +92,7 @@ function getManifestPath( destPath: string, type: "source" | "translation" ): string { - return path.join(process.cwd(), MANIFESTS_DIR, destPath, `${type}.json`) + return path.join(MANIFESTS_DIR, destPath, `${type}.json`) } /** @@ -596,7 +596,10 @@ async function main() { for (const file of englishFiles) { for (const locale of targetLanguages) { const destPath = getDestinationFromPath(file.path, locale) - const smPath = getManifestPath(destPath, "source") + const smPath = path.join( + process.cwd(), + getManifestPath(destPath, "source") + ) const localePath = readLocalePath( destPath, file.type, From 9ea10c769e98a41c843626ade53ebe9ebb4a5559 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:16:26 -0700 Subject: [PATCH 034/109] refactor(intl-pipeline): LLM adapter abstraction Define LlmAdapter interface in lib/llm/adapters.ts with models, name, coAuthor, isAvailable. Active adapter selected in constants.ts. Removes hardcoded Gemini references from main.ts, config.ts, commits. Commit messages: i18n(lang): LLM translation with Co-Authored-By from adapter. Co-Authored-By: Claude Opus 4.6 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- src/scripts/intl-pipeline/config.ts | 6 --- src/scripts/intl-pipeline/constants.ts | 7 ++- .../intl-pipeline/lib/github/commits.ts | 3 +- src/scripts/intl-pipeline/lib/llm/adapters.ts | 30 +++++++++++++ src/scripts/intl-pipeline/lib/llm/gemini.ts | 15 +++---- src/scripts/intl-pipeline/main.ts | 43 +++++++------------ 6 files changed, 61 insertions(+), 43 deletions(-) create mode 100644 src/scripts/intl-pipeline/lib/llm/adapters.ts diff --git a/src/scripts/intl-pipeline/config.ts b/src/scripts/intl-pipeline/config.ts index cf11d22038f..7dd43d781c1 100644 --- a/src/scripts/intl-pipeline/config.ts +++ b/src/scripts/intl-pipeline/config.ts @@ -4,12 +4,6 @@ import i18nConfig from "../../../i18n.config.json" dotenv.config({ path: ".env.local" }) -// Gemini model configuration (single source of truth) -// GEMINI_MODEL env var overrides; otherwise tries models in order -export const GEMINI_MODELS: string[] = process.env.GEMINI_MODEL - ? [process.env.GEMINI_MODEL] - : ["gemini-3.1-pro-preview", "gemini-3.1-pro"] - // Glossary API (ETHGlossary) export const GLOSSARY_API_URL = process.env.GLOSSARY_API_URL || diff --git a/src/scripts/intl-pipeline/constants.ts b/src/scripts/intl-pipeline/constants.ts index 6a9765b69b5..d092feef109 100644 --- a/src/scripts/intl-pipeline/constants.ts +++ b/src/scripts/intl-pipeline/constants.ts @@ -2,9 +2,14 @@ * Pipeline constants -- no side effects, safe to import from tests. */ +import { adapters, type LlmAdapter } from "./lib/llm/adapters" + +// Active LLM adapter (change this to switch LLM providers) +export const LLM: LlmAdapter = adapters.gemini + // Chunk size budget for LLM calls (bytes) // 64KB ~= 16K tokens (English) or 32-64K tokens (CJK) -// Well within Gemini 3.1 Pro's 65K output token limit +// Well within the 65K output token limit // Conservative: prefer more calls over larger chunks export const MAX_CHUNK_BYTES = 65_536 diff --git a/src/scripts/intl-pipeline/lib/github/commits.ts b/src/scripts/intl-pipeline/lib/github/commits.ts index ea74da71e4d..3edf4f5f238 100644 --- a/src/scripts/intl-pipeline/lib/github/commits.ts +++ b/src/scripts/intl-pipeline/lib/github/commits.ts @@ -1,6 +1,7 @@ // GitHub commit operations import { config, gitHubBearerHeaders } from "../../config" +import { LLM } from "../../constants" import { fetchWithRetry } from "../utils/fetch" import { debugLog, delay } from "../workflows/utils" @@ -227,7 +228,7 @@ export class SharedCommitter { method: "POST", headers: { ...gitHubBearerHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ - message: `i18n(${lang}): Gemini translation`, + message: `i18n(${lang}): LLM translation\n\nCo-Authored-By: ${LLM.coAuthor || LLM.name}`, tree: treeData.sha, parents: [parentSha], }), diff --git a/src/scripts/intl-pipeline/lib/llm/adapters.ts b/src/scripts/intl-pipeline/lib/llm/adapters.ts new file mode 100644 index 00000000000..94201e5c491 --- /dev/null +++ b/src/scripts/intl-pipeline/lib/llm/adapters.ts @@ -0,0 +1,30 @@ +/** + * LLM adapter interface and registry. + * + * Each adapter declares its models, identity, and availability. + * The pipeline references the active adapter -- not a specific LLM. + */ + +export interface LlmAdapter { + /** Display name for logs and PR metadata */ + name: string + /** Models to try in order (first available wins) */ + models: string[] + /** Co-author line for git commits (if available) */ + coAuthor?: string + /** Check if this adapter's API key is available */ + isAvailable: () => boolean +} + +// --------------------------------------------------------------------------- +// Available adapters +// --------------------------------------------------------------------------- + +export const adapters: Record = { + gemini: { + name: "Gemini", + models: ["gemini-3.1-pro-preview", "gemini-3.1-pro"], + coAuthor: "Gemini ", + isAvailable: () => Boolean(process.env.GEMINI_API_KEY), + }, +} diff --git a/src/scripts/intl-pipeline/lib/llm/gemini.ts b/src/scripts/intl-pipeline/lib/llm/gemini.ts index 8e6d3a86a8c..f2fe2b7c688 100644 --- a/src/scripts/intl-pipeline/lib/llm/gemini.ts +++ b/src/scripts/intl-pipeline/lib/llm/gemini.ts @@ -8,7 +8,7 @@ import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from "@google/genai" import i18nConfig from "../../../../../i18n.config.json" -import { GEMINI_MODELS } from "../../config" +import { LLM } from "../../constants" import { delay } from "../workflows/utils" import { @@ -36,13 +36,12 @@ import { import { buildTranslationPrompt } from "./prompt-builder" /** - * Check if Gemini API is available (API key present) + * Check if the active LLM is available (API key present) */ -export function isGeminiAvailable(): boolean { - return Boolean(process.env.GEMINI_API_KEY) +export function isLlmAvailable(): boolean { + return LLM.isAvailable() } -// GEMINI_MODELS imported from ../../config const MAX_RETRIES = 3 const RETRY_DELAY_MS = 5000 @@ -1015,7 +1014,7 @@ export async function callGeminiRaw( const verbose = process.env.VERBOSE === "true" const ts = () => new Date().toISOString() - const modelsToTry = GEMINI_MODELS + const modelsToTry = LLM.models // Build context string for log lines const ctx = [ @@ -1187,8 +1186,8 @@ export async function callGeminiRaw( if (modelNotFound.size === modelsToTry.length) { throw new Error( - `All Gemini models unavailable (${[...modelNotFound].join(", ")}). ` + - `Update GEMINI_MODELS in config.ts or set GEMINI_MODEL env var.` + `All ${LLM.name} models unavailable (${[...modelNotFound].join(", ")}). ` + + `Update models in lib/llm/adapters.ts.` ) } diff --git a/src/scripts/intl-pipeline/main.ts b/src/scripts/intl-pipeline/main.ts index 651056c0bb7..7fa0b474976 100644 --- a/src/scripts/intl-pipeline/main.ts +++ b/src/scripts/intl-pipeline/main.ts @@ -2,9 +2,9 @@ * Incremental Translation Pipeline -- Entry Point * * Modes: - * "full" -- Translate entire files from scratch via Gemini + * "full" -- Translate entire files from scratch via LLM * "auto" -- Detect drift since last run; propagate inert changes by script, - * send only changed prose to Gemini (default) + * send only changed prose to LLM (default) * * Environment variables: see config.ts */ @@ -28,11 +28,7 @@ import { mergeBranchInto, } from "./lib/github/branches" import { getDestinationFromPath, SharedCommitter } from "./lib/github/commits" -import { - callGeminiRaw, - isGeminiAvailable, - translateFile, -} from "./lib/llm/gemini" +import { callGeminiRaw, isLlmAvailable, translateFile } from "./lib/llm/gemini" import { batchSections, buildIncrementalPrompt, @@ -55,13 +51,8 @@ import { createTaskPool } from "./lib/utils/task-pool" import { createOrUpdateTranslationPR } from "./lib/workflows/pr-creation" import { sanitizeTranslations } from "./lib/workflows/sanitization" import { logSection } from "./lib/workflows/utils" -import { - config, - GEMINI_MODELS, - GLOSSARY_API_URL, - validateTargetPath, -} from "./config" -import { MANIFESTS_DIR } from "./constants" +import { config, GLOSSARY_API_URL, validateTargetPath } from "./config" +import { LLM, MANIFESTS_DIR } from "./constants" import type { LlmTranslator } from "./pipeline" import { pipeline, PIPELINE_CONFIG } from "./pipeline" @@ -201,7 +192,7 @@ function printTokenSummary( `${"TOTAL".padEnd(10)}| ${pad(String(grandCalls), 5)} | ${pad(fmt(grandInput), 10)} | ${pad(fmt(grandOutput), 10)} | ${pad(fmt(grandTotal), 10)}` ) - // Approximate cost (Gemini 3.1 Pro standard tier, <=200k prompts) + // Approximate cost (standard tier, <=200k prompts) // https://ai.google.dev/gemini-api/docs/pricing (as of 11-April-2026) const INPUT_RATE = 2.0 const OUTPUT_RATE = 12.0 @@ -211,13 +202,13 @@ function printTokenSummary( const pipelineSecs = (pipelineDurationMs / 1000).toFixed(1) console.log( - `\n Estimated cost: ~$${estCost.toFixed(4)} (${GEMINI_MODELS[0]}: $${INPUT_RATE}/1M input, $${OUTPUT_RATE}/1M output)` + `\n Estimated cost: ~$${estCost.toFixed(4)} (${LLM.models[0]}: $${INPUT_RATE}/1M input, $${OUTPUT_RATE}/1M output)` ) console.log(` Wall time: ${pipelineSecs}s`) } /** - * Build an LLM translator that batches section translations via Gemini. + * Build an LLM translator that batches section translations. * Uses batchSections for byte-size-aware splitting of large section lists. */ async function buildGeminiTranslator( @@ -301,7 +292,7 @@ async function buildGeminiTranslator( }) log( - ` Calling Gemini: ${batchSectionList.filter((s) => s.action === "TRANSLATE").length} sections, ${prompt.length} chars` + ` Calling LLM: ${batchSectionList.filter((s) => s.action === "TRANSLATE").length} sections, ${prompt.length} chars` ) const result = await callGeminiRaw(prompt, { @@ -324,12 +315,12 @@ async function buildGeminiTranslator( const translatedIds = Object.keys(allTranslations) log( - ` Gemini returned ${translatedIds.length} sections (${totalInput} in, ${totalOutput} out)` + ` LLM returned ${translatedIds.length} sections (${totalInput} in, ${totalOutput} out)` ) for (const id of sectionIds) { if (!allTranslations[id]) { - console.warn(` Section "${id}" not returned by Gemini`) + console.warn(` Section "${id}" not returned by LLM`) } } @@ -500,13 +491,11 @@ async function runIncremental( const englishB = file.content const llmSectionIds = getLlmSectionIds(englishA, englishB, file.type) - log( - `[${locale}] ${file.path}: ${llmSectionIds.length} section(s) need Gemini` - ) + log(`[${locale}] ${file.path}: ${llmSectionIds.length} section(s) need LLM`) let translator: LlmTranslator | undefined let tokens = { input: 0, output: 0 } - if (llmSectionIds.length > 0 && isGeminiAvailable()) { + if (llmSectionIds.length > 0 && isLlmAvailable()) { const geminiResult = await buildGeminiTranslator( englishB, localeContent, @@ -619,8 +608,8 @@ async function main() { : "no manifest" log(`[${locale}] ${file.path}: ${reason} -> full translation`) - if (!isGeminiAvailable()) { - console.warn(`[${locale}] Skipping: GEMINI_API_KEY not set`) + if (!isLlmAvailable()) { + console.warn(`[${locale}] Skipping: LLM API key not set`) continue } @@ -697,7 +686,7 @@ async function main() { await committer.squashByLanguage() } - // Post-processing: sanitize Gemini output + // Post-processing: sanitize LLM output if (committedFiles.length > 0 && !config.stampOnly) { const englishContentMap = new Map( englishFiles.map((f) => [f.path, f.content]) From 75b3fac0601f1a025c99f7c78cd35735bf68d3ae Mon Sep 17 00:00:00 2001 From: Melissa Nelson Date: Thu, 16 Apr 2026 15:31:39 -0400 Subject: [PATCH 035/109] rework to add links back --- app/[locale]/page.tsx | 17 +++ src/components/Homepage/ExploreEthereum.tsx | 129 ++++++++++++++++++ src/components/Homepage/GetStartedGrid.tsx | 5 +- src/components/Homepage/SavingsCarousel.tsx | 19 ++- .../Homepage/SimulatorSection/index.tsx | 9 +- src/intl/en/page-index.json | 23 +++- 6 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 src/components/Homepage/ExploreEthereum.tsx diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index aee24524ca0..c392be36d3e 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,5 +1,6 @@ import { Suspense } from "react" import { pick } from "lodash" +import { ArrowRight } from "lucide-react" import dynamic from "next/dynamic" import { notFound } from "next/navigation" import { @@ -17,6 +18,7 @@ import TrustLogos from "@/components/Homepage/TrustLogos" import I18nProvider from "@/components/I18nProvider" import MainArticle from "@/components/MainArticle" import { TrackedSection } from "@/components/TrackedSection" +import { BaseLink } from "@/components/ui/Link" import { Section, SectionHeader, SectionTag } from "@/components/ui/section" import { getDirection } from "@/lib/utils/direction" @@ -132,6 +134,21 @@ const Page = async (props: { params: Promise }) => {

} + footer={ + + {t("page-index-simulator-cta")} + + + } />
diff --git a/src/components/Homepage/ExploreEthereum.tsx b/src/components/Homepage/ExploreEthereum.tsx new file mode 100644 index 00000000000..63d88515184 --- /dev/null +++ b/src/components/Homepage/ExploreEthereum.tsx @@ -0,0 +1,129 @@ +import { ChevronRight } from "lucide-react" +import { getTranslations } from "next-intl/server" + +import { BaseLink } from "@/components/ui/Link" + +import { cn } from "@/lib/utils/cn" + +type ExploreLink = { + id: string + title: string + description: string + href: string +} + +type ExploreColumn = { + heading: string + links: ExploreLink[] +} + +type ExploreEthereumProps = { + className?: string + eventCategory?: string +} + +const ExploreEthereum = async ({ + className, + eventCategory = "Homepage", +}: ExploreEthereumProps) => { + const t = await getTranslations("page-index") + + const columns: ExploreColumn[] = [ + { + heading: t("page-index-explore-col-essentials"), + links: [ + { + id: "ether", + title: t("page-index-explore-ether-title"), + description: t("page-index-explore-ether-description"), + href: "/what-is-ether/", + }, + { + id: "wallets", + title: t("page-index-explore-wallets-title"), + description: t("page-index-explore-wallets-description"), + href: "/wallets/", + }, + ], + }, + { + heading: t("page-index-explore-col-vision"), + links: [ + { + id: "whitepaper", + title: t("page-index-explore-whitepaper-title"), + description: t("page-index-explore-whitepaper-description"), + href: "/whitepaper/", + }, + { + id: "roadmap", + title: t("page-index-explore-roadmap-title"), + description: t("page-index-explore-roadmap-description"), + href: "/roadmap/", + }, + ], + }, + { + heading: t("page-index-explore-col-community"), + links: [ + { + id: "community", + title: t("page-index-explore-community-title"), + description: t("page-index-explore-community-description"), + href: "/community/", + }, + { + id: "events", + title: t("page-index-explore-events-title"), + description: t("page-index-explore-events-description"), + href: "/community/events/", + }, + ], + }, + ] + + return ( +
+

+ {t("page-index-explore-title")} +

+ +
+ {columns.map((column) => ( +
+

+ {column.heading} +

+
+ {column.links.map((link) => ( + +
+ + {link.title} + + + {link.description} + +
+ +
+ ))} +
+
+ ))} +
+
+ ) +} + +export default ExploreEthereum diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx index f6337c4a5c0..25330650828 100644 --- a/src/components/Homepage/GetStartedGrid.tsx +++ b/src/components/Homepage/GetStartedGrid.tsx @@ -1,6 +1,7 @@ import { Book, Building2, ChevronRight, Code } from "lucide-react" import { getLocale, getTranslations } from "next-intl/server" +import ExploreEthereum from "@/components/Homepage/ExploreEthereum" import { Image } from "@/components/Image" import { Card, CardContent } from "@/components/ui/card" import { LinkBox, LinkOverlay } from "@/components/ui/link-box" @@ -85,7 +86,7 @@ const GetStartedGrid = async ({ return (
-
+
{t("page-index-get-started-title")} @@ -163,6 +164,8 @@ const GetStartedGrid = async ({ ))}
+ +
) diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index 14d4c343c01..b53cced4c29 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -46,7 +46,7 @@ type Slide = { tag: string title: string subtitle: string - description: string + description: string | React.ReactNode cta: string href: string image: typeof defiImage @@ -104,7 +104,22 @@ function useSlides(): Slide[] { wireFee, days: fmt(5), }), - description: t("page-index-carousel-remittances-description", { txFee }), + description: t.rich("page-index-carousel-remittances-description", { + txFee, + stablecoinsLink: (chunks) => ( + + {chunks} + + ), + }), cta: t("page-index-carousel-remittances-cta"), href: "/payments/", image: remittancesImage, diff --git a/src/components/Homepage/SimulatorSection/index.tsx b/src/components/Homepage/SimulatorSection/index.tsx index b6093f438aa..a65cce6d4b1 100644 --- a/src/components/Homepage/SimulatorSection/index.tsx +++ b/src/components/Homepage/SimulatorSection/index.tsx @@ -17,6 +17,7 @@ import { useWalletOnboardingSimData } from "@/data/WalletSimulatorData" type SimulatorSectionProps = { className?: string header?: React.ReactNode + footer?: React.ReactNode } /** @@ -28,7 +29,11 @@ const SimulatorSkeleton = () => ( ) -const SimulatorSection = ({ className, header }: SimulatorSectionProps) => { +const SimulatorSection = ({ + className, + header, + footer, +}: SimulatorSectionProps) => { const walletOnboardingSimData = useWalletOnboardingSimData() const sendReceiveData = walletOnboardingSimData[SEND_RECEIVE] const { ref: sectionRef, isIntersecting: isVisible } = @@ -86,6 +91,8 @@ const SimulatorSection = ({ className, header }: SimulatorSectionProps) => { )} + + {footer}
) } diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index e1676fddd0f..f2612fc0d83 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -50,7 +50,7 @@ "page-index-carousel-remittances-tag": "CROSS-BORDER PAYMENTS", "page-index-carousel-remittances-title": "Send money home in {minutes} minutes", "page-index-carousel-remittances-subtitle": "Skip the {wireFee} wire fee and the {days}+ day wait.", - "page-index-carousel-remittances-description": "Send stablecoins to anyone, anywhere in the world, for just {txFee}. They receive the funds almost instantly.", + "page-index-carousel-remittances-description": "Send stablecoins to anyone, anywhere in the world, for just {txFee}. They receive the funds almost instantly.", "page-index-carousel-remittances-cta": "Try it yourself →", "page-index-carousel-remittances-traditional-label": "WIRE TRANSFER", "page-index-carousel-remittances-traditional-value": "{min}-{max} days", @@ -115,5 +115,22 @@ "page-index-get-started-enterprise-bullet-1": "Enterprise use cases", "page-index-get-started-enterprise-bullet-2": "Private & permissioned networks", "page-index-get-started-enterprise-bullet-3": "Institutional resources", - "page-index-get-started-enterprise-cta": "Explore enterprise" -} + "page-index-get-started-enterprise-cta": "Explore enterprise", + "page-index-simulator-cta": "Try more guides", + "page-index-explore-title": "More to discover", + "page-index-explore-col-essentials": "Essentials", + "page-index-explore-col-vision": "Vision", + "page-index-explore-col-community": "Community", + "page-index-explore-ether-title": "What is ether (ETH)?", + "page-index-explore-ether-description": "The currency that powers Ethereum.", + "page-index-explore-wallets-title": "Wallets", + "page-index-explore-wallets-description": "How wallets work and why you need one.", + "page-index-explore-whitepaper-title": "Whitepaper", + "page-index-explore-whitepaper-description": "The original proposal that started Ethereum.", + "page-index-explore-roadmap-title": "Roadmap", + "page-index-explore-roadmap-description": "How Ethereum is being improved over time.", + "page-index-explore-community-title": "Community hub", + "page-index-explore-community-description": "Join the global Ethereum community.", + "page-index-explore-events-title": "Events", + "page-index-explore-events-description": "Conferences, hackathons, and meetups." +} \ No newline at end of file From a3ad8eb4134c3228a7f061a0a23e6af3b8893e85 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:34:28 -0700 Subject: [PATCH 036/109] fix(intl-pipeline): address follow-up P1 findings - Validate TARGET_PATH before filesystem reads - Wire AbortController signal to Gemini SDK call - stamp-only tasks now trigger squash/merge/PR - Sanitize all glossary fields (english, translation, note) against prompt injection Co-Authored-By: Claude Opus 4.6 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- src/scripts/intl-pipeline/lib/llm/gemini.ts | 6 +++- src/scripts/intl-pipeline/main.ts | 34 +++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/scripts/intl-pipeline/lib/llm/gemini.ts b/src/scripts/intl-pipeline/lib/llm/gemini.ts index f2fe2b7c688..061affb7b17 100644 --- a/src/scripts/intl-pipeline/lib/llm/gemini.ts +++ b/src/scripts/intl-pipeline/lib/llm/gemini.ts @@ -1070,7 +1070,11 @@ export async function callGeminiRaw( .generateContent({ model: modelId, contents: prompt, - config: { temperature: 0, safetySettings: SAFETY_SETTINGS }, + config: { + temperature: 0, + safetySettings: SAFETY_SETTINGS, + abortSignal: controller.signal, + }, }) .finally(() => clearTimeout(timeout)) const usage = response.usageMetadata diff --git a/src/scripts/intl-pipeline/main.ts b/src/scripts/intl-pipeline/main.ts index 7fa0b474976..386f04bf891 100644 --- a/src/scripts/intl-pipeline/main.ts +++ b/src/scripts/intl-pipeline/main.ts @@ -115,18 +115,21 @@ async function loadGlossary( note?: string }> } + // Sanitize all glossary fields to prevent prompt injection + // eslint-disable-next-line no-control-regex + const controlCharRe = new RegExp("[\\u0000-\\u001f]", "g") + const sanitize = (s: string, maxLen: number) => + s.replace(controlCharRe, "").replace(/\n/g, " ").slice(0, maxLen) + const map = new Map() for (const term of data.terms) { - // Sanitize note to prevent prompt injection (strip control chars, limit length) - // eslint-disable-next-line no-control-regex - const controlCharRe = new RegExp("[\\u0000-\\u001f]", "g") - const safeNote = term.note - ? term.note.replace(controlCharRe, "").slice(0, 200) - : "" + const safeEnglish = sanitize(term.english, 200) + const safeTranslation = sanitize(term.translation, 500) + const safeNote = term.note ? sanitize(term.note, 200) : "" const value = safeNote - ? `${term.translation} (${safeNote})` - : term.translation - map.set(term.english, value) + ? `${safeTranslation} (${safeNote})` + : safeTranslation + map.set(safeEnglish, value) } return map } catch (err) { @@ -563,6 +566,12 @@ async function main() { await committer.init() const committedFiles: Array<{ path: string; content: string }> = [] + let hasCommits = false + + // Validate target paths before any filesystem reads + for (const fp of config.targetPaths) { + validateTargetPath(fp) + } // Load English files from disk const englishFiles: FileContext[] = config.targetPaths.map((fp) => ({ @@ -647,6 +656,7 @@ async function main() { sourceManifest, locale ) + hasCommits = true }) continue } @@ -682,7 +692,7 @@ async function main() { } // Squash interleaved commits into one per language - if (committedFiles.length > 0) { + if (committedFiles.length > 0 || hasCommits) { await committer.squashByLanguage() } @@ -701,7 +711,7 @@ async function main() { } // Merge temp branch into target branch - if (committedFiles.length > 0) { + if (committedFiles.length > 0 || hasCommits) { log(`Merging ${tempBranch} -> ${targetBranch}`) await ensureStagingBranch(targetBranch, baseBranch) const merged = await mergeBranchInto(tempBranch, targetBranch) @@ -716,7 +726,7 @@ async function main() { } // Create or update PR unless skipped - if (committedFiles.length > 0 && !config.skipPr) { + if ((committedFiles.length > 0 || hasCommits) && !config.skipPr) { const languagePairs = targetLanguages.map((code) => { const entry = i18nConfig.find((l: { code: string }) => l.code === code) return { From 413faed46b506cd4db973a75b99eb1e7ecd4e49f Mon Sep 17 00:00:00 2001 From: Melissa Nelson Date: Thu, 16 Apr 2026 16:09:15 -0400 Subject: [PATCH 037/109] updating metadata --- app/[locale]/page-jsonld.tsx | 7 +++---- src/intl/en/page-index.json | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/[locale]/page-jsonld.tsx b/app/[locale]/page-jsonld.tsx index 84b76665673..97a02214214 100644 --- a/app/[locale]/page-jsonld.tsx +++ b/app/[locale]/page-jsonld.tsx @@ -32,14 +32,13 @@ export default async function IndexPageJsonLD({ url: url, name: "ethereum.org", description: t("page-index-meta-description"), - educationalUse: "Independent Study", + educationalUse: "Self-Paced", keywords: - "Ethereum, Blockchain, Smart Contracts, Web3, Open Source, Protocol, Documentation, Education", + "Ethereum, ETH, Crypto, Digital Ownership, DeFi, Decentralized Finance, Privacy, Stablecoins, Web3, Blockchain, Smart Contracts, Open Source", inLanguage: locale, license: "https://opensource.org/licenses/MIT", audience: { "@type": "EducationalAudience", - educationalRole: ["developer", "student"], audienceType: "public", }, publisher: ethereumFoundationReference, @@ -49,7 +48,7 @@ export default async function IndexPageJsonLD({ "@type": "Thing", name: "Ethereum", description: - "A decentralized, open-source blockchain with smart contract functionality.", + "Ethereum is a global, open-source blockchain network with smart contract functionality, and a platform that powers digital ownership, decentralized finance (DeFi), and privacy-preserving applications.", image: "https://ethereum.org/images/assets/eth-diamond-glyph.png", sameAs: [ "https://www.wikidata.org/wiki/Q16783523", diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index f2612fc0d83..3209ef58010 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -1,6 +1,6 @@ { - "page-index-meta-title": "Ethereum.org: The complete guide to Ethereum", - "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.", + "page-index-meta-title": "Ethereum - The complete guide from Ethereum.org", + "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you control your own money, data, and identity. No bank, no middleman, no permission needed.", "page-index-description": "The leading platform for innovative apps and blockchain networks", "page-index-title": "Welcome to Ethereum", "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.", From 707fc789f42752a1db4ae0a78b69d84d5ea508c1 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 13:45:52 +0200 Subject: [PATCH 038/109] hide external link icon on explore enterprise homepage cta --- src/components/Homepage/GetStartedGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx index db5ae77fa6d..d5be463852c 100644 --- a/src/components/Homepage/GetStartedGrid.tsx +++ b/src/components/Homepage/GetStartedGrid.tsx @@ -149,6 +149,7 @@ const GetStartedGrid = async ({ Date: Fri, 17 Apr 2026 14:17:27 +0200 Subject: [PATCH 039/109] remove unused ctaVariant direct-buttons variant from HomeHero --- src/components/Hero/HomeHero/index.tsx | 116 +++---------------------- src/components/Hero/index.ts | 2 +- src/intl/en/page-index.json | 7 -- 3 files changed, 12 insertions(+), 113 deletions(-) diff --git a/src/components/Hero/HomeHero/index.tsx b/src/components/Hero/HomeHero/index.tsx index 9d44eb1235c..31f641d72f6 100644 --- a/src/components/Hero/HomeHero/index.tsx +++ b/src/components/Hero/HomeHero/index.tsx @@ -1,4 +1,4 @@ -import { Fragment, Suspense } from "react" +import { Suspense } from "react" import dynamic from "next/dynamic" import { getImageProps, type StaticImageData } from "next/image" import { getTranslations } from "next-intl/server" @@ -12,13 +12,6 @@ import { Button } from "@/components/ui/buttons/Button" const PersonaModalCTA = dynamic( () => import("@/components/Homepage/PersonaModalCTA") ) -import EthGlyphIcon from "@/components/icons/eth-glyph.svg" -import EthTokenIcon from "@/components/icons/eth-token.svg" -import EthWalletIcon from "@/components/icons/eth-wallet.svg" -import TryAppsIcon from "@/components/icons/phone-homescreen.svg" -import SvgButtonLink, { - type SvgButtonLinkProps, -} from "@/components/ui/buttons/SvgButtonLink" import { cn } from "@/lib/utils/cn" import { breakpointAsNumber } from "@/lib/utils/screen" @@ -26,13 +19,10 @@ import { breakpointAsNumber } from "@/lib/utils/screen" import heroBase from "@/public/images/home/hero.png" import hero2xl from "@/public/images/home/hero-2xl.png" -export type CTAVariant = "modal" | "direct-buttons" - type HomeHeroProps = ClassNameProp & { image?: StaticImageData image2xl?: StaticImageData alt?: string - ctaVariant?: CTAVariant eventCategory?: string } @@ -41,7 +31,6 @@ const HomeHero = async ({ image, image2xl, alt: altProp, - ctaVariant = "modal", eventCategory = "Homepage", }: HomeHeroProps) => { const t = await getTranslations("page-index") @@ -49,41 +38,6 @@ const HomeHero = async ({ const xlImage = image2xl ?? image ?? hero2xl const alt = altProp ?? t("page-index-hero-image-alt") - const directButtonCTAs = [ - { - label: t("page-index-cta-learn-label"), - description: t("page-index-modal-what-is-ethereum"), - href: "/what-is-ethereum/", - Svg: EthGlyphIcon, - className: "text-accent-a hover:text-accent-a-hover", - eventName: "learn_ethereum", - }, - { - label: t("page-index-cta-wallet-label"), - description: t("page-index-cta-wallet-description"), - href: "/wallets/find-wallet/", - Svg: EthWalletIcon, - className: "text-primary hover:text-primary-hover", - eventName: "pick_wallet", - }, - { - label: t("page-index-cta-get-eth-label"), - description: t("page-index-cta-get-eth-description"), - href: "/get-eth/", - Svg: EthTokenIcon, - className: "text-accent-b hover:text-accent-b-hover", - eventName: "get_eth", - }, - { - label: t("page-index-cta-dapps-label"), - description: t("page-index-cta-dapps-description"), - href: "/dapps/", - Svg: TryAppsIcon, - className: "text-accent-c hover:text-accent-c-hover", - eventName: "try_apps", - }, - ] - const common = { alt, sizes: `(max-width: ${breakpointAsNumber["2xl"]}px) 100vw, ${breakpointAsNumber["2xl"]}px`, @@ -140,64 +94,16 @@ const HomeHero = async ({ {t("page-index-hero-subtitle")}

- {ctaVariant === "modal" ? ( - - {t("page-index-hero-cta")} - - - } - > - - - ) : ( -
- {directButtonCTAs.map( - ({ - label, - description, - href, - className: ctaClass, - Svg, - eventName, - }) => { - const Link = ( - props: Omit< - SvgButtonLinkProps, - "Svg" | "href" | "label" | "children" - > - ) => ( - -

{description}

-
- ) - return ( - - - - - ) - } - )} -
- )} + + {t("page-index-hero-cta")} + + + } + > + + diff --git a/src/components/Hero/index.ts b/src/components/Hero/index.ts index 8df2467aae2..48581c76bd0 100644 --- a/src/components/Hero/index.ts +++ b/src/components/Hero/index.ts @@ -1,5 +1,5 @@ export { default as ContentHero, type ContentHeroProps } from "./ContentHero" -export { type CTAVariant, default as HomeHero } from "./HomeHero" +export { default as HomeHero } from "./HomeHero" export { default as HubHero } from "./HubHero" export { default as MdxHero, type MdxHeroProps } from "./MdxHero" export { diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index 3209ef58010..ed627a92391 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -7,15 +7,8 @@ "page-index-hero-title": "The internet that belongs to you", "page-index-hero-subtitle": "Ethereum is the global network where you control your assets, your data, and your identity.", "page-index-hero-cta": "Start here", - "page-index-cta-wallet-label": "Pick a wallet", - "page-index-cta-wallet-description": "Create accounts & manage assets", - "page-index-cta-get-eth-label": "Get ETH", - "page-index-cta-get-eth-description": "The currency of Ethereum", - "page-index-cta-dapps-label": "Try apps", - "page-index-cta-dapps-description": "See what Ethereum can do", "page-index-cta-build-apps-label": "Start building", "page-index-cta-build-apps-description": "Create your first app", - "page-index-cta-learn-label": "Learn Ethereum", "page-index-modal-title": "What brings you here?", "page-index-modal-description": "Choose your path: resources for beginners, developers, or enterprise.", "page-index-modal-beginners": "For beginners", From 2c2b5d3b7cf29be48c4370760c0f0dc347a377de Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 14:43:22 +0200 Subject: [PATCH 040/109] refactor: consolidate number formatting into shared helpers --- src/components/Homepage/FeatureCards.tsx | 7 ++-- src/components/Homepage/KPISection.tsx | 11 ++---- src/components/Homepage/SavingsCarousel.tsx | 9 ++--- src/components/Homepage/TrustLogos.tsx | 7 ++-- .../Simulator/WalletHome/TokenBalanceItem.tsx | 18 ++++------ .../Simulator/WalletHome/WalletBalance.tsx | 14 +++----- .../screens/SendReceive/ReceivedEther.tsx | 18 +++------- .../screens/SendReceive/SendEther.tsx | 12 +++---- .../screens/SendReceive/SendSummary.tsx | 29 +++++---------- .../Simulator/screens/SendReceive/Success.tsx | 19 +++------- src/components/Simulator/utils.ts | 36 ++++++++++++++++++- src/lib/utils/numbers.ts | 11 ++++++ 12 files changed, 89 insertions(+), 102 deletions(-) diff --git a/src/components/Homepage/FeatureCards.tsx b/src/components/Homepage/FeatureCards.tsx index 19f4fac7129..4b98c6fa334 100644 --- a/src/components/Homepage/FeatureCards.tsx +++ b/src/components/Homepage/FeatureCards.tsx @@ -6,7 +6,7 @@ import { ButtonLink } from "@/components/ui/buttons/Button" import { Section, SectionHeader } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" -import { numberFormat } from "@/lib/utils/numbers" +import { formatCompactNumber } from "@/lib/utils/numbers" import freeAccessImage from "@/public/images/homepage/features/free-access.png" import globalImage from "@/public/images/homepage/features/global.png" @@ -25,10 +25,9 @@ const FeatureCards = async ({ const t = await getTranslations("page-index") const locale = await getLocale() - const volume = numberFormat(locale, { - notation: "compact", + const volume = formatCompactNumber(4_600_000_000, locale, { maximumSignificantDigits: 2, - }).format(4_600_000_000) + }) return (
{formatter(displayValue)}

} -function formatCompact(value: number, locale: string): string { - return numberFormat(locale, { - notation: "compact", - maximumSignificantDigits: 3, - }).format(value) -} - /** * Format transaction count with spaces as thousands separator (design choice * to avoid commas/dots that interfere with the animated counter). @@ -188,7 +181,7 @@ const KPISection = ({

{accountHolders !== null - ? formatCompact(accountHolders, locale) + ? formatCompactNumber(accountHolders, locale) : "—"}

diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index 3550299bb97..d4e1515b579 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -21,7 +21,7 @@ import { import { cn } from "@/lib/utils/cn" import { trackCustomEvent } from "@/lib/utils/matomo" -import { numberFormat } from "@/lib/utils/numbers" +import { formatPriceUSD, numberFormat } from "@/lib/utils/numbers" import FloatingCard from "./FloatingCard" @@ -65,12 +65,7 @@ function useSlides(): Slide[] { currency: "USD", maximumFractionDigits: 0, }) - const txFee = fmt(0.02, { - style: "currency", - currency: "USD", - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }) + const txFee = formatPriceUSD(0.02, locale) const twelve = fmt(12) return [ diff --git a/src/components/Homepage/TrustLogos.tsx b/src/components/Homepage/TrustLogos.tsx index 29c54a4f72b..c7aa1ac1023 100644 --- a/src/components/Homepage/TrustLogos.tsx +++ b/src/components/Homepage/TrustLogos.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/section" import { cn } from "@/lib/utils/cn" -import { numberFormat } from "@/lib/utils/numbers" +import { numberFormat, numberToPercent } from "@/lib/utils/numbers" import FloatingCard from "./FloatingCard" @@ -29,10 +29,7 @@ const TrustLogos = async ({ const t = await getTranslations("page-index") const locale = await getLocale() - const uptime = numberFormat(locale, { - style: "percent", - maximumFractionDigits: 0, - }).format(1) + const uptime = numberToPercent(1, locale) const count = numberFormat(locale).format(10) return ( diff --git a/src/components/Simulator/WalletHome/TokenBalanceItem.tsx b/src/components/Simulator/WalletHome/TokenBalanceItem.tsx index 2552d18fd5f..3c56807fdcb 100644 --- a/src/components/Simulator/WalletHome/TokenBalanceItem.tsx +++ b/src/components/Simulator/WalletHome/TokenBalanceItem.tsx @@ -1,8 +1,8 @@ -import { Flex } from "@/components/ui/flex" +import { useLocale } from "next-intl" -import { numberFormat } from "@/lib/utils/numbers" +import { Flex } from "@/components/ui/flex" -import { getMaxFractionDigitsUsd } from "../utils" +import { formatWalletToken, formatWalletUsd } from "../utils" import { TokenBalance } from "./interfaces" @@ -11,16 +11,10 @@ type TokenBalanceItemProps = { } export const TokenBalanceItem = ({ item }: TokenBalanceItemProps) => { const { name, ticker, amount, usdConversion, Icon } = item + const locale = useLocale() const usdAmount = amount * usdConversion - const usdValue = numberFormat("en-US", { - style: "currency", - currency: "USD", - notation: "compact", - maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), - }).format(usdAmount) - const tokenAmount = numberFormat("en", { - maximumFractionDigits: 5, - }).format(amount) + const usdValue = formatWalletUsd(usdAmount, locale) + const tokenAmount = formatWalletToken(amount, locale) return ( diff --git a/src/components/Simulator/WalletHome/WalletBalance.tsx b/src/components/Simulator/WalletHome/WalletBalance.tsx index eea9b1f1a03..b18226af47b 100644 --- a/src/components/Simulator/WalletHome/WalletBalance.tsx +++ b/src/components/Simulator/WalletHome/WalletBalance.tsx @@ -1,11 +1,9 @@ import React from "react" -import { useTranslations } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" -import { numberFormat } from "@/lib/utils/numbers" - -import { getMaxFractionDigitsUsd } from "../utils" +import { formatWalletUsd } from "../utils" import { AddressPill } from "./AddressPill" @@ -15,18 +13,14 @@ type WalletBalanceProps = { export const WalletBalance = ({ usdAmount = 0 }: WalletBalanceProps) => { const t = useTranslations("component-wallet-simulator") + const locale = useLocale() return (

{t("sim-your-total")}

- {numberFormat("en-US", { - style: "currency", - currency: "USD", - notation: "compact", - maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), - }).format(usdAmount)} + {formatWalletUsd(usdAmount, locale)}

diff --git a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx index 9d5f70849bc..cc7c431e1bc 100644 --- a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx +++ b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx @@ -1,13 +1,11 @@ import { useEffect, useMemo, useState } from "react" import { Info, X } from "lucide-react" import { AnimatePresence, motion } from "motion/react" -import { useTranslations } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import type { SimulatorNavProps } from "@/lib/types" -import { numberFormat } from "@/lib/utils/numbers" - -import { getMaxFractionDigitsUsd } from "../../utils" +import { formatWalletToken, formatWalletUsd } from "../../utils" import { WalletHome } from "../../WalletHome" import type { TokenBalance } from "../../WalletHome/interfaces" @@ -25,6 +23,7 @@ export const ReceivedEther = ({ sender, }: ReceivedEtherProps) => { const t = useTranslations("component-wallet-simulator") + const locale = useLocale() const [received, setReceived] = useState(false) const [hideToast, setHideToast] = useState(false) const showToast = received && !hideToast @@ -64,16 +63,9 @@ export const ReceivedEther = ({ const tokenBalances = received ? tokensWithEthBalance : defaultTokenBalances - const displayEth: string = numberFormat("en", { - maximumFractionDigits: 5, - }).format(ethReceiveAmount) + const displayEth = formatWalletToken(ethReceiveAmount, locale) const usdReceiveAmount = ethReceiveAmount * ethPrice - const displayUsd: string = numberFormat("en", { - style: "currency", - currency: "USD", - notation: "compact", - maximumFractionDigits: getMaxFractionDigitsUsd(usdReceiveAmount), - }).format(usdReceiveAmount) + const displayUsd = formatWalletUsd(usdReceiveAmount, locale) return ( { const t = useTranslations("component-wallet-simulator") + const locale = useLocale() const formatDollars = (amount: number): string => - numberFormat("en-US", { + numberFormat(locale, { style: "currency", currency: "USD", notation: "compact", @@ -33,9 +35,7 @@ export const SendEther = ({ const usdAmount = formatDollars(ethPrice * ethBalance) - const ethAmount = numberFormat("en", { - maximumFractionDigits: 5, - }).format(ethBalance) + const ethAmount = formatWalletToken(ethBalance, locale) const maxUsdAmount = ethPrice * ethBalance @@ -52,7 +52,7 @@ export const SendEther = ({ if (amount === maxUsdAmount) return "Max" return formatDollars(amount) } - const formatChosenAmount = numberFormat("en", { + const formatChosenAmount = numberFormat(locale, { 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 e826b341a2c..73ccb71cc07 100644 --- a/src/components/Simulator/screens/SendReceive/SendSummary.tsx +++ b/src/components/Simulator/screens/SendReceive/SendSummary.tsx @@ -1,13 +1,12 @@ import React from "react" -import { useTranslations } from "next-intl" +import { useLocale, useTranslations } from "next-intl" 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" +import { formatWalletToken, formatWalletUsd } from "../../utils" type SendSummaryProps = { chosenAmount: number @@ -21,16 +20,9 @@ export const SendSummary = ({ recipient, }: SendSummaryProps) => { const t = useTranslations("component-wallet-simulator") + const locale = useLocale() - const formatEth = (amount: number): string => - numberFormat("en", { maximumFractionDigits: 5 }).format(amount) - - const formatChosenAmount = numberFormat("en", { - style: "currency", - currency: "USD", - notation: "compact", - maximumFractionDigits: getMaxFractionDigitsUsd(chosenAmount), - }).format(chosenAmount) + const formatChosenAmount = formatWalletUsd(chosenAmount, locale) const usdFee = ETH_TRANSFER_FEE * ethPrice return ( @@ -51,7 +43,7 @@ export const SendSummary = ({

- {formatEth(chosenAmount / ethPrice)} ETH + {formatWalletToken(chosenAmount / ethPrice, locale)} ETH

{/* Bottom section */} @@ -67,17 +59,12 @@ export const SendSummary = ({

{t("sim-summary-fees")}

- {numberFormat("en", { - maximumFractionDigits: getMaxFractionDigitsUsd(usdFee), - style: "currency", - currency: "USD", - notation: "compact", - }).format(usdFee)} + {formatWalletUsd(usdFee, locale)} ( - {numberFormat("en", { + {formatWalletToken(ETH_TRANSFER_FEE, locale, { 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 1cfdbdbe36d..60e9e3c7e61 100644 --- a/src/components/Simulator/screens/SendReceive/Success.tsx +++ b/src/components/Simulator/screens/SendReceive/Success.tsx @@ -1,15 +1,14 @@ import { useEffect, useState } from "react" import { Check } from "lucide-react" import { AnimatePresence, motion } from "motion/react" -import { useTranslations } from "next-intl" +import { useLocale, useTranslations } from "next-intl" 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 { formatWalletToken, formatWalletUsd } from "../../utils" import { WalletHome } from "../../WalletHome" import type { TokenBalance } from "../../WalletHome/interfaces" @@ -28,22 +27,14 @@ export const Success = ({ recipient, }: SuccessProps) => { const t = useTranslations("component-wallet-simulator") + const locale = useLocale() const [txPending, setTxPending] = useState(true) const [showWallet, setShowWallet] = useState(false) const [categoryIndex, setCategoryIndex] = useState(0) const usdAmount = sentEthAmount * ethPrice - - const usdValue = numberFormat("en", { - style: "currency", - currency: "USD", - notation: "compact", - maximumFractionDigits: getMaxFractionDigitsUsd(usdAmount), - }).format(usdAmount) - - const sentEthValue = numberFormat("en", { - maximumFractionDigits: 5, - }).format(sentEthAmount) + const usdValue = formatWalletUsd(usdAmount, locale) + const sentEthValue = formatWalletToken(sentEthAmount, locale) // Show spinner for defined number of milliseconds, switching "loading" state to false when complete const SPINNER_DURATION = 1000 diff --git a/src/components/Simulator/utils.ts b/src/components/Simulator/utils.ts index 688ded94e50..85a02560e9e 100644 --- a/src/components/Simulator/utils.ts +++ b/src/components/Simulator/utils.ts @@ -1,3 +1,5 @@ +import { numberFormat } from "@/lib/utils/numbers" + import { PATH_IDS } from "./constants" import type { PathId } from "./types" @@ -12,8 +14,40 @@ export const isValidPathId = ( return PATH_IDS.includes(pathIdString as PathId) } -export const getMaxFractionDigitsUsd = (value) => { +export const getMaxFractionDigitsUsd = (value: number): 0 | 2 => { const roundedToCent = Math.round(value * 100) / 100 const isEvenDollar = roundedToCent % 1 === 0 return isEvenDollar ? 0 : 2 } + +/** + * Compact USD formatting for the wallet simulator. Uses getMaxFractionDigitsUsd + * so even dollar amounts render as "$5" instead of "$5.00". + */ +export const formatWalletUsd = ( + value: number, + locale: string, + options?: Intl.NumberFormatOptions +): string => + numberFormat(locale, { + style: "currency", + currency: "USD", + notation: "compact", + maximumFractionDigits: getMaxFractionDigitsUsd(value), + ...options, + }).format(value) + +/** + * Token amount formatting for the wallet simulator. Defaults to 5 fraction + * digits, which is the granularity used by the wallet balance and transfer + * screens. + */ +export const formatWalletToken = ( + value: number, + locale: string, + options?: Intl.NumberFormatOptions +): string => + numberFormat(locale, { + maximumFractionDigits: 5, + ...options, + }).format(value) diff --git a/src/lib/utils/numbers.ts b/src/lib/utils/numbers.ts index 48a108611a8..dd79876310b 100644 --- a/src/lib/utils/numbers.ts +++ b/src/lib/utils/numbers.ts @@ -60,6 +60,17 @@ export const formatLargeNumber = (value: number, locale: string): string => { }).format(value) } +export const formatCompactNumber = ( + value: number, + locale: string, + options?: Intl.NumberFormatOptions +): string => + numberFormat(locale, { + notation: "compact", + maximumSignificantDigits: 3, + ...options, + }).format(value) + export const formatPriceUSD = (value: number, locale: string): string => { return numberFormat(locale, { style: "currency", From 71336b0ec3e9459e1835ea76ff8ec25e17c23412 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 15:09:36 +0200 Subject: [PATCH 041/109] fix: force en-us for simulator usd to preserve phone mockup layout --- .../Simulator/WalletHome/TokenBalanceItem.tsx | 2 +- .../Simulator/WalletHome/WalletBalance.tsx | 5 ++--- .../screens/SendReceive/ReceivedEther.tsx | 2 +- .../screens/SendReceive/SendEther.tsx | 22 +++++-------------- .../screens/SendReceive/SendSummary.tsx | 4 ++-- .../Simulator/screens/SendReceive/Success.tsx | 2 +- src/components/Simulator/utils.ts | 11 ++++++---- 7 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/components/Simulator/WalletHome/TokenBalanceItem.tsx b/src/components/Simulator/WalletHome/TokenBalanceItem.tsx index 3c56807fdcb..0dc533af698 100644 --- a/src/components/Simulator/WalletHome/TokenBalanceItem.tsx +++ b/src/components/Simulator/WalletHome/TokenBalanceItem.tsx @@ -13,7 +13,7 @@ export const TokenBalanceItem = ({ item }: TokenBalanceItemProps) => { const { name, ticker, amount, usdConversion, Icon } = item const locale = useLocale() const usdAmount = amount * usdConversion - const usdValue = formatWalletUsd(usdAmount, locale) + const usdValue = formatWalletUsd(usdAmount) const tokenAmount = formatWalletToken(amount, locale) return ( diff --git a/src/components/Simulator/WalletHome/WalletBalance.tsx b/src/components/Simulator/WalletHome/WalletBalance.tsx index b18226af47b..ba63aeb12fd 100644 --- a/src/components/Simulator/WalletHome/WalletBalance.tsx +++ b/src/components/Simulator/WalletHome/WalletBalance.tsx @@ -1,5 +1,5 @@ import React from "react" -import { useLocale, useTranslations } from "next-intl" +import { useTranslations } from "next-intl" import { Flex } from "@/components/ui/flex" @@ -13,14 +13,13 @@ type WalletBalanceProps = { export const WalletBalance = ({ usdAmount = 0 }: WalletBalanceProps) => { const t = useTranslations("component-wallet-simulator") - const locale = useLocale() return (

{t("sim-your-total")}

- {formatWalletUsd(usdAmount, locale)} + {formatWalletUsd(usdAmount)}

diff --git a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx index cc7c431e1bc..490a49c234a 100644 --- a/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx +++ b/src/components/Simulator/screens/SendReceive/ReceivedEther.tsx @@ -65,7 +65,7 @@ export const ReceivedEther = ({ const displayEth = formatWalletToken(ethReceiveAmount, locale) const usdReceiveAmount = ethReceiveAmount * ethPrice - const displayUsd = formatWalletUsd(usdReceiveAmount, locale) + const displayUsd = formatWalletUsd(usdReceiveAmount) return ( - numberFormat(locale, { - style: "currency", - currency: "USD", - notation: "compact", - }).format(amount) - - const usdAmount = formatDollars(ethPrice * ethBalance) - + const usdAmount = formatWalletUsd(ethPrice * ethBalance) const ethAmount = formatWalletToken(ethBalance, locale) const maxUsdAmount = ethPrice * ethBalance @@ -50,14 +41,11 @@ export const SendEther = ({ const AMOUNTS: Array = [5, 10, 20, maxUsdAmount] const formatButtonLabel = (amount: number): string => { if (amount === maxUsdAmount) return "Max" - return formatDollars(amount) + return formatWalletUsd(amount) } - const formatChosenAmount = numberFormat(locale, { - style: "currency", - currency: "USD", - notation: "compact", + const formatChosenAmount = formatWalletUsd(chosenAmount, { maximumFractionDigits: 0, - }).format(chosenAmount) + }) return (
diff --git a/src/components/Simulator/screens/SendReceive/SendSummary.tsx b/src/components/Simulator/screens/SendReceive/SendSummary.tsx index 73ccb71cc07..2d13e2b8036 100644 --- a/src/components/Simulator/screens/SendReceive/SendSummary.tsx +++ b/src/components/Simulator/screens/SendReceive/SendSummary.tsx @@ -22,7 +22,7 @@ export const SendSummary = ({ const t = useTranslations("component-wallet-simulator") const locale = useLocale() - const formatChosenAmount = formatWalletUsd(chosenAmount, locale) + const formatChosenAmount = formatWalletUsd(chosenAmount) const usdFee = ETH_TRANSFER_FEE * ethPrice return ( @@ -59,7 +59,7 @@ export const SendSummary = ({

{t("sim-summary-fees")}

- {formatWalletUsd(usdFee, locale)} + {formatWalletUsd(usdFee)} ( {formatWalletToken(ETH_TRANSFER_FEE, locale, { diff --git a/src/components/Simulator/screens/SendReceive/Success.tsx b/src/components/Simulator/screens/SendReceive/Success.tsx index 60e9e3c7e61..d647ee458f6 100644 --- a/src/components/Simulator/screens/SendReceive/Success.tsx +++ b/src/components/Simulator/screens/SendReceive/Success.tsx @@ -33,7 +33,7 @@ export const Success = ({ const [categoryIndex, setCategoryIndex] = useState(0) const usdAmount = sentEthAmount * ethPrice - const usdValue = formatWalletUsd(usdAmount, locale) + const usdValue = formatWalletUsd(usdAmount) const sentEthValue = formatWalletToken(sentEthAmount, locale) // Show spinner for defined number of milliseconds, switching "loading" state to false when complete diff --git a/src/components/Simulator/utils.ts b/src/components/Simulator/utils.ts index 85a02560e9e..5926798ebec 100644 --- a/src/components/Simulator/utils.ts +++ b/src/components/Simulator/utils.ts @@ -21,15 +21,18 @@ export const getMaxFractionDigitsUsd = (value: number): 0 | 2 => { } /** - * Compact USD formatting for the wallet simulator. Uses getMaxFractionDigitsUsd - * so even dollar amounts render as "$5" instead of "$5.00". + * Compact USD formatting for the wallet simulator. Always uses en-US so the + * demo wallet shows the universally-recognizable "$" symbol regardless of + * the page locale — non-English currency formats (e.g. "50 US$" in Spanish, + * "50 $US" in French) overflow the fixed-width phone mockup. ETH amounts + * still format per page locale via formatWalletToken. + * Uses getMaxFractionDigitsUsd so even dollar amounts render as "$5" not "$5.00". */ export const formatWalletUsd = ( value: number, - locale: string, options?: Intl.NumberFormatOptions ): string => - numberFormat(locale, { + numberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", From 03a51a5da65af2a92add19ce9ef9f33bdae318e2 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 15:43:16 +0200 Subject: [PATCH 042/109] revert more to discover section from homepage --- src/components/Homepage/ExploreEthereum.tsx | 129 -------------------- src/components/Homepage/GetStartedGrid.tsx | 5 +- src/intl/en/page-index.json | 20 +-- 3 files changed, 3 insertions(+), 151 deletions(-) delete mode 100644 src/components/Homepage/ExploreEthereum.tsx diff --git a/src/components/Homepage/ExploreEthereum.tsx b/src/components/Homepage/ExploreEthereum.tsx deleted file mode 100644 index 63d88515184..00000000000 --- a/src/components/Homepage/ExploreEthereum.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { ChevronRight } from "lucide-react" -import { getTranslations } from "next-intl/server" - -import { BaseLink } from "@/components/ui/Link" - -import { cn } from "@/lib/utils/cn" - -type ExploreLink = { - id: string - title: string - description: string - href: string -} - -type ExploreColumn = { - heading: string - links: ExploreLink[] -} - -type ExploreEthereumProps = { - className?: string - eventCategory?: string -} - -const ExploreEthereum = async ({ - className, - eventCategory = "Homepage", -}: ExploreEthereumProps) => { - const t = await getTranslations("page-index") - - const columns: ExploreColumn[] = [ - { - heading: t("page-index-explore-col-essentials"), - links: [ - { - id: "ether", - title: t("page-index-explore-ether-title"), - description: t("page-index-explore-ether-description"), - href: "/what-is-ether/", - }, - { - id: "wallets", - title: t("page-index-explore-wallets-title"), - description: t("page-index-explore-wallets-description"), - href: "/wallets/", - }, - ], - }, - { - heading: t("page-index-explore-col-vision"), - links: [ - { - id: "whitepaper", - title: t("page-index-explore-whitepaper-title"), - description: t("page-index-explore-whitepaper-description"), - href: "/whitepaper/", - }, - { - id: "roadmap", - title: t("page-index-explore-roadmap-title"), - description: t("page-index-explore-roadmap-description"), - href: "/roadmap/", - }, - ], - }, - { - heading: t("page-index-explore-col-community"), - links: [ - { - id: "community", - title: t("page-index-explore-community-title"), - description: t("page-index-explore-community-description"), - href: "/community/", - }, - { - id: "events", - title: t("page-index-explore-events-title"), - description: t("page-index-explore-events-description"), - href: "/community/events/", - }, - ], - }, - ] - - return ( -

-

- {t("page-index-explore-title")} -

- -
- {columns.map((column) => ( -
-

- {column.heading} -

-
- {column.links.map((link) => ( - -
- - {link.title} - - - {link.description} - -
- -
- ))} -
-
- ))} -
-
- ) -} - -export default ExploreEthereum diff --git a/src/components/Homepage/GetStartedGrid.tsx b/src/components/Homepage/GetStartedGrid.tsx index d5be463852c..598d97da830 100644 --- a/src/components/Homepage/GetStartedGrid.tsx +++ b/src/components/Homepage/GetStartedGrid.tsx @@ -1,7 +1,6 @@ import { Book, Building2, ChevronRight, Code } from "lucide-react" import { getLocale, getTranslations } from "next-intl/server" -import ExploreEthereum from "@/components/Homepage/ExploreEthereum" import { Image } from "@/components/Image" import { Card, CardContent } from "@/components/ui/card" import { LinkBox, LinkOverlay } from "@/components/ui/link-box" @@ -86,7 +85,7 @@ const GetStartedGrid = async ({ return (
-
+
{t("page-index-get-started-title")} @@ -165,8 +164,6 @@ const GetStartedGrid = async ({ ))}
- -
) diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index ed627a92391..6afec899743 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -109,21 +109,5 @@ "page-index-get-started-enterprise-bullet-2": "Private & permissioned networks", "page-index-get-started-enterprise-bullet-3": "Institutional resources", "page-index-get-started-enterprise-cta": "Explore enterprise", - "page-index-simulator-cta": "Try more guides", - "page-index-explore-title": "More to discover", - "page-index-explore-col-essentials": "Essentials", - "page-index-explore-col-vision": "Vision", - "page-index-explore-col-community": "Community", - "page-index-explore-ether-title": "What is ether (ETH)?", - "page-index-explore-ether-description": "The currency that powers Ethereum.", - "page-index-explore-wallets-title": "Wallets", - "page-index-explore-wallets-description": "How wallets work and why you need one.", - "page-index-explore-whitepaper-title": "Whitepaper", - "page-index-explore-whitepaper-description": "The original proposal that started Ethereum.", - "page-index-explore-roadmap-title": "Roadmap", - "page-index-explore-roadmap-description": "How Ethereum is being improved over time.", - "page-index-explore-community-title": "Community hub", - "page-index-explore-community-description": "Join the global Ethereum community.", - "page-index-explore-events-title": "Events", - "page-index-explore-events-description": "Conferences, hackathons, and meetups." -} \ No newline at end of file + "page-index-simulator-cta": "Try more guides" +} From ef144023014875607759dd793b39696c59ee7dd4 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 15:44:04 +0200 Subject: [PATCH 043/109] restore default link color on stablecoins inline link so it reads as a link --- src/components/Homepage/SavingsCarousel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Homepage/SavingsCarousel.tsx b/src/components/Homepage/SavingsCarousel.tsx index d4e1515b579..54f4f73b7d3 100644 --- a/src/components/Homepage/SavingsCarousel.tsx +++ b/src/components/Homepage/SavingsCarousel.tsx @@ -104,7 +104,7 @@ function useSlides(): Slide[] { stablecoinsLink: (chunks) => ( Date: Fri, 17 Apr 2026 17:45:03 +0200 Subject: [PATCH 044/109] match hero heading size to section headings on mobile --- src/components/Hero/HomeHero/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Hero/HomeHero/index.tsx b/src/components/Hero/HomeHero/index.tsx index 31f641d72f6..8f893d0c259 100644 --- a/src/components/Hero/HomeHero/index.tsx +++ b/src/components/Hero/HomeHero/index.tsx @@ -86,7 +86,7 @@ const HomeHero = async ({
-

+

{t("page-index-hero-title")}

From 6271953fed40c433acb8b1990938fb90a1ac3138 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 17:57:24 +0200 Subject: [PATCH 045/109] chore: remove orphan homepage components left over from 2026 redesign --- .../_components/HomepageLazyImports.tsx | 66 ----- src/components/Homepage/BentoCard.tsx | 66 ----- src/components/Homepage/BentoCardSwiper.tsx | 58 ---- src/components/Homepage/CodeExamples.tsx | 174 ------------ .../Homepage/HomepageSectionImage.tsx | 110 -------- src/components/Homepage/RecentPostsSwiper.tsx | 84 ------ .../Homepage/ValuesMarquee/Fallback.tsx | 35 --- .../Homepage/ValuesMarquee/index.tsx | 254 ------------------ src/components/Homepage/utils.ts | 144 ---------- 9 files changed, 991 deletions(-) delete mode 100644 app/[locale]/_components/HomepageLazyImports.tsx delete mode 100644 src/components/Homepage/BentoCard.tsx delete mode 100644 src/components/Homepage/BentoCardSwiper.tsx delete mode 100644 src/components/Homepage/CodeExamples.tsx delete mode 100644 src/components/Homepage/HomepageSectionImage.tsx delete mode 100644 src/components/Homepage/RecentPostsSwiper.tsx delete mode 100644 src/components/Homepage/ValuesMarquee/Fallback.tsx delete mode 100644 src/components/Homepage/ValuesMarquee/index.tsx delete mode 100644 src/components/Homepage/utils.ts diff --git a/app/[locale]/_components/HomepageLazyImports.tsx b/app/[locale]/_components/HomepageLazyImports.tsx deleted file mode 100644 index 8d9a6792e3e..00000000000 --- a/app/[locale]/_components/HomepageLazyImports.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client" - -import dynamic from "next/dynamic" - -import ValuesMarqueeFallback from "@/components/Homepage/ValuesMarquee/Fallback" -import { Skeleton, SkeletonCardGrid } from "@/components/ui/skeleton" - -export const BentoCardSwiper = dynamic( - () => import("@/components/Homepage/BentoCardSwiper"), - { - ssr: false, - loading: () => ( -
- - -
- ), - } -) - -export const RecentPostsSwiper = dynamic( - () => import("@/components/Homepage/RecentPostsSwiper"), - { - ssr: false, - loading: () => ( -
- - -
- ), - } -) - -export const ValuesMarquee = dynamic( - () => import("@/components/Homepage/ValuesMarquee"), - { - ssr: false, - loading: () => , - } -) - -export const CodeExamples = dynamic( - () => import("@/components/Homepage/CodeExamples"), - { - ssr: false, - loading: () => ( -
- -
- ), - } -) - -export const AppsHighlight = dynamic( - () => import("../apps/_components/AppsHighlight"), - { - ssr: false, - loading: () => ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- ), - } -) diff --git a/src/components/Homepage/BentoCard.tsx b/src/components/Homepage/BentoCard.tsx deleted file mode 100644 index ae68ddb4c58..00000000000 --- a/src/components/Homepage/BentoCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { HTMLAttributes } from "react" -import { type StaticImageData } from "next/image" - -import { Image } from "@/components/Image" -import { ButtonLink } from "@/components/ui/buttons/Button" - -import { cn } from "@/lib/utils/cn" - -import { ChevronNext } from "../Chevron" -import { Card, CardTitle } from "../ui/card" -import { Center } from "../ui/flex" - -export type BentoCardProps = HTMLAttributes & { - action: string - href: string - imgSrc: StaticImageData - imgWidth?: number - imgHeight?: number - title: string - eventName: string - eventCategory: string -} - -const BentoCard = ({ - action, - children, - className, - href, - imgSrc, - imgWidth, - imgHeight, - title, - eventName, - eventCategory, -}: BentoCardProps) => ( - -
- -
-
- - {title} - -

{children}

- - {action} - -
-
-) - -export default BentoCard diff --git a/src/components/Homepage/BentoCardSwiper.tsx b/src/components/Homepage/BentoCardSwiper.tsx deleted file mode 100644 index 7cb57c77558..00000000000 --- a/src/components/Homepage/BentoCardSwiper.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import { SwiperSlide } from "swiper/react" - -import { cn } from "@/lib/utils/cn" -import { trackCustomEvent } from "@/lib/utils/matomo" - -import { Swiper, SwiperContainer, SwiperNavigation } from "../ui/swiper" - -import BentoCard from "./BentoCard" -import { BentoItem } from "./utils" - -type BentoCardSwiperProps = { - bentoItems: BentoItem[] - eventCategory: string -} - -const BentoCardSwiper = ({ - bentoItems, - eventCategory, -}: BentoCardSwiperProps) => ( - - { - trackCustomEvent({ - eventCategory, - eventAction: "cta_swipe", - eventName: String(activeIndex + 1), - }) - }} - > - {bentoItems.map(({ className, ...item }) => ( - - - - ))} - - - -) - -BentoCardSwiper.displayName = "BentoCardSwiper" - -export default BentoCardSwiper diff --git a/src/components/Homepage/CodeExamples.tsx b/src/components/Homepage/CodeExamples.tsx deleted file mode 100644 index 42a7901f0fb..00000000000 --- a/src/components/Homepage/CodeExamples.tsx +++ /dev/null @@ -1,174 +0,0 @@ -"use client" - -import { useCallback, useEffect, useState } from "react" -import { Clipboard, ClipboardCheck } from "lucide-react" -import { useLocale } from "next-intl" - -import type { CodeExample } from "@/lib/interfaces" - -import AngleBrackets from "@/components/icons/angle-brackets.svg" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" - -import { cn } from "@/lib/utils/cn" -import { trackCustomEvent } from "@/lib/utils/matomo" - -import Codeblock from "../Codeblock" -import CodeModal from "../CodeModal" -import CopyToClipboard from "../CopyToClipboard" -import { SkeletonLines } from "../ui/skeleton" -import WindowBox from "../WindowBox" - -type CodeExamplesProps = { - codeExamples: CodeExample[] - title: string - eventCategory: string -} - -const AccordionCodeBlock = ({ - code, - codeLanguage, -}: { - code: string - codeLanguage: string -}) => ( - <> - - {code} - - - {(hasCopied) => (hasCopied ? : )} - - -) - -const CodeExamples = ({ title, codeExamples }: CodeExamplesProps) => { - const locale = useLocale() - - const [isModalOpen, setModalOpen] = useState(false) - const [activeCode, setActiveCode] = useState(0) - const [fetchedCodes, setFetchedCodes] = useState<{ [key: number]: string }>( - {} - ) - - const eventCategory = `Homepage - ${locale}` - - const getCode = useCallback( - (idx: number) => { - const example = codeExamples[idx] - if (!fetchedCodes[idx]) { - fetch(example.codeUrl) - .then((res) => res.text()) - .then((text) => setFetchedCodes((prev) => ({ ...prev, [idx]: text }))) - } - }, - [codeExamples, fetchedCodes] - ) - - // For modal: fetch code when opened if needed - useEffect(() => { - if (isModalOpen) { - getCode(activeCode) - } - }, [isModalOpen, activeCode, getCode]) - - // For accordion: fetch code when expanded if needed - const handleAccordionOpen = (idx: number) => { - getCode(idx) - } - - return ( -
- }> - {/* Desktop */} - {codeExamples.map(({ title, description, eventName }, idx) => ( - - ))} - {/* Mobile */} - - {codeExamples.map(({ title, description, codeLanguage }, idx) => ( - - handleAccordionOpen(idx)} - > -
-

- {title} -

-

- {description} -

-
-
- -
- {!fetchedCodes[idx] ? ( - - ) : ( - - )} -
-
-
- ))} -
-
- - {!fetchedCodes[activeCode] ? ( - - ) : ( - - {fetchedCodes[activeCode]} - - )} - -
- ) -} - -CodeExamples.displayName = "CodeExamples" - -export default CodeExamples diff --git a/src/components/Homepage/HomepageSectionImage.tsx b/src/components/Homepage/HomepageSectionImage.tsx deleted file mode 100644 index ca4ebb5e948..00000000000 --- a/src/components/Homepage/HomepageSectionImage.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import type { StaticImageData } from "next/image" -import { getImageProps } from "next/image" - -import { breakpointAsNumber } from "@/lib/utils/screen" - -// Import all landscape images (all PNG now) -import developersHubHero from "@/public/images/heroes/developers-hub-hero.png" -// Import all portrait images (all PNG) -import developersHubHeroPortrait from "@/public/images/heroes/developers-hub-hero-portrait.png" -import layerTwoHubHero from "@/public/images/heroes/layer-2-hub-hero.png" -import layerTwoHubHeroPortrait from "@/public/images/heroes/layer-2-hub-hero-portrait.png" -import learnHubHero from "@/public/images/heroes/learn-hub-hero.png" -import learnHubHeroPortrait from "@/public/images/heroes/learn-hub-hero-portrait.png" -import quizzesHubHero from "@/public/images/heroes/quizzes-hub-hero.png" -import quizzesHubHeroPortrait from "@/public/images/heroes/quizzes-hub-hero-portrait.png" - -const imageMap: Record< - string, - { - mobile: StaticImageData - desktop: StaticImageData - } -> = { - "what-is-ethereum": { - mobile: learnHubHero, - desktop: learnHubHeroPortrait, - }, - "what-is-ether": { - mobile: quizzesHubHero, - desktop: quizzesHubHeroPortrait, - }, - activity: { - mobile: layerTwoHubHero, - desktop: layerTwoHubHeroPortrait, - }, - learn: { - mobile: learnHubHero, - desktop: learnHubHeroPortrait, - }, - builders: { - mobile: developersHubHero, - desktop: developersHubHeroPortrait, - }, - community: { - mobile: quizzesHubHero, - desktop: quizzesHubHeroPortrait, - }, -} - -type HomepageSectionImageProps = { - sectionId: string // e.g. "activity", "learn", "builders", "community" - alt: string - className?: string -} - -export default function HomepageSectionImage({ - sectionId, - alt, - className, -}: HomepageSectionImageProps) { - const images = imageMap[sectionId] - - if (!images) { - throw new Error( - `Image not found for section: ${sectionId}. Available sections: ${Object.keys(imageMap).join(", ")}` - ) - } - - const common = { - alt, - // Be very specific: Desktop max 512px, Mobile 100vw - sizes: `(max-width: ${breakpointAsNumber.md}px) 100vw, 512px`, - priority: false, - } - - const { - props: { srcSet: desktop }, - } = getImageProps({ - ...common, - ...images.desktop, - quality: 35, - }) - - const { - props: { srcSet: mobile, ...rest }, - } = getImageProps({ - ...common, - ...images.mobile, - quality: 40, - }) - - // Remove blurWidth/blurHeight from rest to avoid React DOM warnings - // (Next.js getImageProps includes them but they're not valid HTML attributes) - delete (rest as Record).blurWidth - delete (rest as Record).blurHeight - - return ( - - - - {alt} - - ) -} diff --git a/src/components/Homepage/RecentPostsSwiper.tsx b/src/components/Homepage/RecentPostsSwiper.tsx deleted file mode 100644 index dc25474eb9b..00000000000 --- a/src/components/Homepage/RecentPostsSwiper.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client" - -import type { RSSItem } from "@/lib/types" - -import { isValidDate } from "@/lib/utils/date" -import { breakpointAsNumber } from "@/lib/utils/screen" - -import CardImage from "../Image/CardImage" -import { - Card, - CardBanner, - CardContent, - CardParagraph, - CardTitle, -} from "../ui/card" -import { - Swiper, - SwiperContainer, - SwiperNavigation, - SwiperSlide, -} from "../ui/swiper" - -type RecentPostsSwiperProps = { - rssItems: RSSItem[] - eventCategory: string - className?: string -} - -const RecentPostsSwiper = ({ - rssItems, - eventCategory, - className, -}: RecentPostsSwiperProps) => ( - - - {rssItems.map(({ pubDate, title, source, link, imgSrc }) => ( - - - - - - - {title} - {isValidDate(pubDate) && ( - - {pubDate} - - )} - - {source} - - - - - ))} - - - -) - -RecentPostsSwiper.displayName = "RecentPostsSwiper" - -export default RecentPostsSwiper diff --git a/src/components/Homepage/ValuesMarquee/Fallback.tsx b/src/components/Homepage/ValuesMarquee/Fallback.tsx deleted file mode 100644 index 8d1543d7341..00000000000 --- a/src/components/Homepage/ValuesMarquee/Fallback.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Fragment } from "react" - -import { Skeleton } from "@/components/ui/skeleton" -import { Spinner } from "@/components/ui/spinner" - -const ValuesMarqueeFallback = () => ( -
- -
- {Array.from({ length: 10 }).map((_, i) => ( - - - - - ))} -
-
- -
- {Array.from({ length: 10 }).map((_, i) => ( - - - - - ))} -
- - -
-
-) - -ValuesMarqueeFallback.displayName = "ValuesMarqueeFallback" - -export default ValuesMarqueeFallback diff --git a/src/components/Homepage/ValuesMarquee/index.tsx b/src/components/Homepage/ValuesMarquee/index.tsx deleted file mode 100644 index 70469715291..00000000000 --- a/src/components/Homepage/ValuesMarquee/index.tsx +++ /dev/null @@ -1,254 +0,0 @@ -"use client" - -import { forwardRef, useEffect, useRef, useState } from "react" -import { Check, X } from "lucide-react" - -import type { ValuesPairing } from "@/lib/types" - -import EthGlyphSolid from "@/components/icons/eth-glyph-solid.svg" -import Tooltip from "@/components/Tooltip" - -import { cn } from "@/lib/utils/cn" -import { isMobile } from "@/lib/utils/isMobile" -import { trackCustomEvent } from "@/lib/utils/matomo" - -import { Stack } from "../../ui/flex" - -import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion" -import { useRtlFlip } from "@/hooks/useRtlFlip" - -type ItemProps = React.HTMLAttributes & { - pairing: ValuesPairing - separatorClass: string - container?: HTMLElement | null - label: string - eventCategory: string - direction: HTMLDivElement["dir"] -} - -const Item = ({ - children, - className, - pairing, - separatorClass, - container, - label, - eventCategory, - direction, -}: ItemProps) => ( - <> - { - trackCustomEvent({ - eventCategory, - eventAction: "internet_changing", - eventName: label, - }) - }} - content={ - -

- {label} -

-
-
-
- -
-
- {pairing.legacy.content.map((line) => ( -

- {line} -

- ))} -
-
-
-
- -
-
- {pairing.ethereum.content.map((line) => ( -

- {line} -

- ))} -
-
-
-
- } - > -
- {children} -
-
-
- -) -Item.displayName = "MarqueeItem" - -type RowProps = React.HTMLAttributes & { - toRight?: boolean -} - -const Row = forwardRef( - ({ className, children, toRight }, ref) => { - const { prefersReducedMotion } = usePrefersReducedMotion() - - const fadeEdges = { - mask: `linear-gradient(to right, transparent 1rem, white 15%, white 85%, transparent calc(100% - 1rem))`, - } - - return ( - // Note: dir="ltr" forced on parent to prevent "translateX" animation bugs - // Locale "direction" passed to marquee Item for correction -
-
- {Array(prefersReducedMotion ? 1 : 3) - .fill(0) - .map((_, idx) => ( -
- {children} -
- ))} -
-
- ) - } -) -Row.displayName = "MarqueeRow" - -type ValuesMarqueeProps = { - pairings: ValuesPairing[] - eventCategory: string - categoryLabels: { - ethereum: string - legacy: string - } -} - -const ValuesMarquee = ({ - pairings, - eventCategory, - categoryLabels, -}: ValuesMarqueeProps) => { - const containerFirstRef = useRef(null) - const containerSecondRef = useRef(null) - - const [containerFirst, setContainerFirst] = useState( - null - ) - const [containerSecond, setContainerSecond] = useState( - null - ) - - useEffect(() => { - if (containerFirstRef.current) { - setContainerFirst(containerFirstRef.current) - } - if (containerSecondRef.current) { - setContainerSecond(containerSecondRef.current) - } - }, []) - - const { direction, isRtl, twFlipForRtl } = useRtlFlip() - - return ( -
- - {pairings.map((pairing) => ( - - - {pairing.ethereum.label} - - ))} - - - {pairings.map((pairing) => ( - - {pairing.legacy.label} - - ))} - -
-

- {categoryLabels.legacy} -

-
-

- {categoryLabels.ethereum} -

-
-
- ) -} -ValuesMarquee.displayName = "ValuesMarquee" - -export default ValuesMarquee diff --git a/src/components/Homepage/utils.ts b/src/components/Homepage/utils.ts deleted file mode 100644 index 5452833d005..00000000000 --- a/src/components/Homepage/utils.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { StaticImageData } from "next/image" -import { getTranslations } from "next-intl/server" - -import { cn } from "@/lib/utils/cn" - -import ImpactImage from "@/public/images/impact_transparent.png" -import ManAndDogImage from "@/public/images/man-and-dog-playing.png" -import ManBabyWomanImage from "@/public/images/man-baby-woman.png" -import RobotBarImage from "@/public/images/robot-help-bar.png" -import MergeImage from "@/public/images/upgrades/merge.png" - -type Breakpoint = "mobile" | "lg" | "xl" -type Direction = "down" | "up" | "right" | "left" -type Color = "primary" | "accent-a" | "accent-b" | "accent-c" -type Category = "stablecoins" | "defi" | "dapps" | "networks" | "assets" -type CopyDetails = { - title: string - children: string - action: string - href: string - eventName: Category -} -export type BentoItem = CopyDetails & { - imgSrc: StaticImageData - imgWidth?: number - className: string -} - -const gradientStops = "from-20% to-60%" - -const colorOptions: Record = { - primary: cn( - gradientStops, - "from-primary/10 to-primary/5 dark:from-primary/20 dark:to-primary/10 border-primary/10" - ), - "accent-a": cn( - gradientStops, - "from-accent-a/10 to-accent-a/5 dark:from-accent-a/20 dark:to-accent-a/10 border-accent-a/10" - ), - "accent-b": cn( - gradientStops, - "from-accent-b/10 to-accent-b/5 dark:from-accent-b/20 dark:to-accent-b/10 border-accent-b/10" - ), - "accent-c": cn( - gradientStops, - "from-accent-c/10 to-accent-c/5 dark:from-accent-c/20 dark:to-accent-c/10 border-accent-c/10" - ), -} - -const flow: Record> = { - mobile: { - down: "flex-col bg-gradient-to-b", - up: "flex-col-reverse bg-gradient-to-t", - right: "flex-row bg-gradient-to-r", - left: "flex-row-reverse bg-gradient-to-l", - }, - lg: { - down: "lg:flex-col lg:bg-gradient-to-b", - up: "lg:flex-col-reverse lg:bg-gradient-to-t", - right: "lg:flex-row lg:bg-gradient-to-r", - left: "lg:flex-row-reverse lg:bg-gradient-to-l", - }, - xl: { - down: "xl:flex-col xl:bg-gradient-to-b", - up: "xl:flex-col-reverse xl:bg-gradient-to-t", - right: "xl:flex-row xl:bg-gradient-to-r", - left: "xl:flex-row-reverse xl:bg-gradient-to-l", - }, -} - -const stylesByPosition: Record = { - mobile: [ - flow.mobile.down, - flow.mobile.down, - flow.mobile.down, - flow.mobile.down, - flow.mobile.down, - ], - lg: [ - cn("lg:col-span-6 lg:row-start-2", flow.lg.up), - cn("lg:col-span-6 lg:col-start-7 lg:row-start-2", flow.lg.down), - cn("lg:col-span-12 lg:row-start-3", flow.lg.right), - cn("lg:col-span-6 lg:col-start-7 lg:row-start-4", flow.lg.up), - cn("lg:col-span-6 lg:row-start-4", flow.lg.down), - ], - xl: [ - cn("xl:col-span-8 xl:col-start-5 xl:row-start-1", flow.xl.right), - cn("xl:col-span-4 xl:row-start-2", flow.xl.up), - cn("xl:col-span-4 xl:col-start-5 xl:row-start-2", flow.xl.down), - cn("xl:col-span-4 xl:col-start-9 xl:row-span-2 xl:row-start-2", flow.xl.up), - cn("xl:col-span-8 xl:row-start-3", flow.xl.right), - ], -} - -const getPosition = (position: number): string => - cn( - stylesByPosition.mobile[position], - stylesByPosition.lg[position], - stylesByPosition.xl[position] - ) - -export const getBentoBoxItems = async (): Promise => { - const t = await getTranslations("page-index") - - const getCopy = (category: Category, href: string): CopyDetails => ({ - title: t(`page-index-bento-${category}-title`), - children: t(`page-index-bento-${category}-content`), - action: t(`page-index-bento-${category}-action`), - href, - eventName: category, - }) - - return [ - { - ...getCopy("stablecoins", "/stablecoins/"), - imgSrc: ManAndDogImage, - className: cn(colorOptions["primary"], getPosition(0)), - }, - { - ...getCopy("defi", "/defi/"), - imgSrc: ImpactImage, - imgWidth: 400, - className: cn(colorOptions["accent-c"], getPosition(1)), - }, - { - ...getCopy("networks", "/layer-2/"), - imgSrc: MergeImage, - imgWidth: 320, - className: cn(colorOptions["accent-b"], getPosition(2)), - }, - { - ...getCopy("dapps", "/apps/"), - imgSrc: ManBabyWomanImage, - imgWidth: 324, - className: cn(colorOptions["accent-a"], getPosition(3)), - }, - { - ...getCopy("assets", "/nft/"), - imgSrc: RobotBarImage, - imgWidth: 324, - className: cn(colorOptions["primary"], getPosition(4)), - }, - ] -} From 6b9d0eca7eca1aa8b106a0e0aac000f348dfba5f Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 18:27:21 +0200 Subject: [PATCH 046/109] rtl: flip simulator arrows and close buttons in rtl locales --- app/[locale]/page.tsx | 5 +++-- src/components/Simulator/Explanation.tsx | 5 ++++- src/components/Simulator/MoreInfoPopover.tsx | 2 +- src/components/Simulator/NotificationPopover.tsx | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index e92d627b0a0..0d6b32168fb 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -19,6 +19,7 @@ import { TrackedSection } from "@/components/TrackedSection" import { BaseLink } from "@/components/ui/Link" import { SectionHeader, SectionTag } from "@/components/ui/section" +import { cn } from "@/lib/utils/cn" import { getDirection } from "@/lib/utils/direction" import { getMetadata } from "@/lib/utils/metadata" @@ -59,7 +60,7 @@ const Page = async (props: { params: Promise }) => { const transactionsToday = "value" in growThePieData.txCount ? growThePieData.txCount.value : null - const { direction: dir } = getDirection(locale) + const { direction: dir, twFlipForRtl } = getDirection(locale) const t = await getTranslations("page-index") const allMessages = await getMessages() const glossary = allMessages["glossary-tooltip"] as Record @@ -130,7 +131,7 @@ const Page = async (props: { params: Promise }) => { }} > {t("page-index-simulator-cta")} - + } /> diff --git a/src/components/Simulator/Explanation.tsx b/src/components/Simulator/Explanation.tsx index 860319784f5..6d54f8837a7 100644 --- a/src/components/Simulator/Explanation.tsx +++ b/src/components/Simulator/Explanation.tsx @@ -19,6 +19,8 @@ import { MoreInfoPopover } from "./MoreInfoPopover" import { PathButton } from "./PathButton" import type { PathId } from "./types" +import { useRtlFlip } from "@/hooks/useRtlFlip" + type ExplanationProps = SimulatorNavProps & { explanation: SimulatorExplanation nextPathSummary: SimulatorPathSummary | null @@ -37,6 +39,7 @@ export const Explanation = ({ logFinalCta, }: ExplanationProps) => { const t = useTranslations("component-wallet-simulator") + const { twFlipForRtl } = useRtlFlip() const { regressStepper, step, totalSteps } = nav const { header, description } = explanation @@ -63,7 +66,7 @@ export const Explanation = ({ variants={backButtonVariants} animate={step === 0 ? "hidden" : "visible"} > - + {t("sim-back")} diff --git a/src/components/Simulator/MoreInfoPopover.tsx b/src/components/Simulator/MoreInfoPopover.tsx index 705d94080ad..d86252a53ad 100644 --- a/src/components/Simulator/MoreInfoPopover.tsx +++ b/src/components/Simulator/MoreInfoPopover.tsx @@ -38,7 +38,7 @@ export const MoreInfoPopover = ({ isFirstStep, children }: MoreInfoPopover) => { className="relative start-4 w-[calc(100vw-3rem)] max-w-xs bg-background-highlight px-4 py-6 text-sm shadow-none sm:start-8 sm:w-[calc(100vw-5rem)]" data-testid="more-info-popover-content" > - +
{children}
diff --git a/src/components/Simulator/NotificationPopover.tsx b/src/components/Simulator/NotificationPopover.tsx index 0ae69155c4e..3ab2cc6d553 100644 --- a/src/components/Simulator/NotificationPopover.tsx +++ b/src/components/Simulator/NotificationPopover.tsx @@ -35,7 +35,7 @@ export const NotificationPopover = ({
{title || ""}
- + From 73c8c7c5bbfcdc601ce58662034908fb30cd2d0f Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Fri, 17 Apr 2026 18:31:29 +0200 Subject: [PATCH 047/109] a11y: make sr-only crawler nav non-selectable --- src/components/Homepage/PersonaModalCTA.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Homepage/PersonaModalCTA.tsx b/src/components/Homepage/PersonaModalCTA.tsx index 1720a27f5e9..51c444e856a 100644 --- a/src/components/Homepage/PersonaModalCTA.tsx +++ b/src/components/Homepage/PersonaModalCTA.tsx @@ -233,7 +233,7 @@ const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => { {/* Static links for SEO — these URLs are only reachable via the JS dialog modal above, so we render them visually-hidden to ensure crawlers can discover them. */} -