Skip to content

Commit fb83efc

Browse files
authored
Merge pull request #1015 from IncrediboyXD/feat/extract-copy-strings-950-951-952-953
refactor: extract hardcoded UI strings into lib/copy modules
2 parents 0076711 + f4cd97f commit fb83efc

7 files changed

Lines changed: 221 additions & 88 deletions

File tree

‎components/landing/hero.tsx‎

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import {
1212
DropdownMenuTrigger,
1313
} from '@/components/ui/dropdown-menu'
1414
import { LiveStreamPreview } from '@/components/landing/live-stream-preview'
15+
import { landingHeaderCopy, landingHeroCopy } from '@/lib/copy/landing'
1516

1617
const NAV_LINKS = [
17-
{ href: '#features', label: 'Features' },
18-
{ href: '#how', label: 'How it works' },
19-
{ href: '#use-cases', label: 'Use cases' },
18+
{ href: '#features', label: landingHeaderCopy.navLinks.features },
19+
{ href: '#how', label: landingHeaderCopy.navLinks.how },
20+
{ href: '#use-cases', label: landingHeaderCopy.navLinks.useCases },
2021
]
2122

2223
export function LandingHeader() {
@@ -41,7 +42,7 @@ export function LandingHeader() {
4142
variant="ghost"
4243
size="icon"
4344
className="md:hidden"
44-
aria-label="Open navigation menu"
45+
aria-label={landingHeaderCopy.openMenuAriaLabel}
4546
>
4647
{mobileNavOpen ? <X className="size-5" /> : <Menu className="size-5" />}
4748
</Button>
@@ -52,7 +53,7 @@ export function LandingHeader() {
5253
className="min-w-56 rounded-xl border border-border bg-background/95 p-2 shadow-xl backdrop-blur-md"
5354
>
5455
<div className="mb-1 px-2 py-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
55-
Navigate
56+
{landingHeaderCopy.mobileMenuHeading}
5657
</div>
5758
{NAV_LINKS.map((link) => (
5859
<DropdownMenuItem
@@ -69,7 +70,7 @@ export function LandingHeader() {
6970

7071
<Button asChild size="sm">
7172
<Link href="/app">
72-
Open app
73+
{landingHeaderCopy.openAppButton}
7374
<ArrowRight className="size-4" />
7475
</Link>
7576
</Button>
@@ -91,29 +92,26 @@ export function Hero() {
9192
<div>
9293
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1 text-xs text-muted-foreground">
9394
<span className="size-1.5 rounded-full bg-primary" />
94-
Built on Stellar &amp; Soroban
95+
{landingHeroCopy.badge}
9596
</span>
9697
<h1 className="mt-5 text-balance text-4xl font-semibold leading-[1.05] tracking-tight sm:text-6xl">
97-
Stream money by the second
98+
{landingHeroCopy.title}
9899
</h1>
99100
<p className="mt-5 max-w-md text-pretty text-lg leading-relaxed text-muted-foreground">
100-
FlowStar turns one-time transfers into continuous streams. Perfect for payroll, token
101-
vesting, and grants that unlock in real time — withdraw anytime, cancel anytime.
101+
{landingHeroCopy.description}
102102
</p>
103103
<div className="mt-8 flex flex-wrap items-center gap-3">
104104
<Button asChild size="lg">
105105
<Link href="/app/create">
106-
Create a stream
106+
{landingHeroCopy.createStreamButton}
107107
<ArrowRight className="size-4" />
108108
</Link>
109109
</Button>
110110
<Button asChild size="lg" variant="secondary">
111-
<Link href="/app">Open dashboard</Link>
111+
<Link href="/app">{landingHeroCopy.openDashboardButton}</Link>
112112
</Button>
113113
</div>
114-
<p className="mt-6 font-mono text-xs text-muted-foreground">
115-
No code · Non-custodial · Testnet ready
116-
</p>
114+
<p className="mt-6 font-mono text-xs text-muted-foreground">{landingHeroCopy.tagline}</p>
117115
</div>
118116

119117
<div className="relative">

‎components/landing/sections.tsx‎

Lines changed: 22 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -12,78 +12,33 @@ import {
1212
} from 'lucide-react'
1313
import { Button } from '@/components/ui/button'
1414
import { Brand, APP_NAME } from '@/components/brand'
15+
import { landingSectionsCopy } from '@/lib/copy/landing'
16+
17+
const { features, howItWorks, useCases, cta, footer } = landingSectionsCopy
1518

1619
const FEATURES = [
17-
{
18-
icon: Gauge,
19-
title: 'Per-second unlocking',
20-
body: 'Funds unlock continuously down to the second. Recipients see their balance grow in real time, no waiting for payout dates.',
21-
},
22-
{
23-
icon: ShieldCheck,
24-
title: 'Non-custodial',
25-
body: 'Streams live in a Soroban smart contract. FlowStar never holds your keys or your tokens — you sign every action from your wallet.',
26-
},
27-
{
28-
icon: Hourglass,
29-
title: 'Cliffs & schedules',
30-
body: 'Add a cliff before unlocking starts, set start and end times, and let the contract handle the math precisely.',
31-
},
32-
{
33-
icon: Wallet,
34-
title: 'Withdraw anytime',
35-
body: 'Recipients withdraw unlocked funds whenever they want. Senders can cancel and reclaim whatever has not unlocked yet.',
36-
},
20+
{ icon: Gauge, ...features.items.perSecond },
21+
{ icon: ShieldCheck, ...features.items.nonCustodial },
22+
{ icon: Hourglass, ...features.items.cliffs },
23+
{ icon: Wallet, ...features.items.withdraw },
3724
]
3825

3926
const USE_CASES = [
40-
{
41-
icon: Briefcase,
42-
title: 'Payroll',
43-
body: 'Pay contributors a salary that streams every second instead of monthly lump sums.',
44-
},
45-
{
46-
icon: CalendarClock,
47-
title: 'Token vesting',
48-
body: 'Vest team and investor allocations on-chain with cliffs and linear schedules.',
49-
},
50-
{
51-
icon: Gift,
52-
title: 'Grants',
53-
body: 'Fund builders progressively and keep the ability to stop if milestones slip.',
54-
},
55-
{
56-
icon: Coins,
57-
title: 'Subscriptions',
58-
body: 'Bill continuously for services with streams that anyone can verify on-chain.',
59-
},
27+
{ icon: Briefcase, ...useCases.items.payroll },
28+
{ icon: CalendarClock, ...useCases.items.vesting },
29+
{ icon: Gift, ...useCases.items.grants },
30+
{ icon: Coins, ...useCases.items.subscriptions },
6031
]
6132

62-
const STEPS = [
63-
{
64-
step: '01',
65-
title: 'Connect your wallet',
66-
body: 'Sign in with Freighter, xBull, LOBSTR, or Albedo. FlowStar reads your address — nothing more.',
67-
},
68-
{
69-
step: '02',
70-
title: 'Create a stream',
71-
body: 'Pick a token, recipient, amount, and schedule. Fund it in a single transaction.',
72-
},
73-
{
74-
step: '03',
75-
title: 'Watch it flow',
76-
body: 'The recipient withdraws unlocked funds anytime. Cancel to reclaim the rest whenever you need.',
77-
},
78-
]
33+
const STEPS = [howItWorks.steps.connect, howItWorks.steps.create, howItWorks.steps.watch]
7934

8035
export function Features() {
8136
return (
8237
<section id="features" className="mx-auto max-w-6xl px-4 py-20 sm:px-6">
8338
<div className="max-w-2xl">
84-
<p className="text-sm font-medium text-primary">Why FlowStar</p>
39+
<p className="text-sm font-medium text-primary">{features.eyebrow}</p>
8540
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
86-
Programmable money that moves continuously
41+
{features.title}
8742
</h2>
8843
</div>
8944
<div className="mt-12 grid gap-4 sm:grid-cols-2">
@@ -111,9 +66,9 @@ export function HowItWorks() {
11166
<section id="how" className="border-y border-border bg-card/30">
11267
<div className="mx-auto max-w-6xl px-4 py-20 sm:px-6">
11368
<div className="max-w-2xl">
114-
<p className="text-sm font-medium text-primary">How it works</p>
69+
<p className="text-sm font-medium text-primary">{howItWorks.eyebrow}</p>
11570
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
116-
Three steps from transfer to stream
71+
{howItWorks.title}
11772
</h2>
11873
</div>
11974
<div className="mt-12 grid gap-8 md:grid-cols-3">
@@ -137,9 +92,9 @@ export function UseCases() {
13792
return (
13893
<section id="use-cases" className="mx-auto max-w-6xl px-4 py-20 sm:px-6">
13994
<div className="max-w-2xl">
140-
<p className="text-sm font-medium text-primary">Use cases</p>
95+
<p className="text-sm font-medium text-primary">{useCases.eyebrow}</p>
14196
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
142-
One primitive, many payment flows
97+
{useCases.title}
14398
</h2>
14499
</div>
145100
<div className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
@@ -168,16 +123,15 @@ export function CTA() {
168123
className="pointer-events-none absolute -bottom-24 left-1/2 size-[420px] -translate-x-1/2 rounded-full bg-primary/10 blur-3xl"
169124
/>
170125
<h2 className="relative text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
171-
Start streaming in minutes
126+
{cta.title}
172127
</h2>
173128
<p className="relative mx-auto mt-4 max-w-md text-pretty text-muted-foreground">
174-
Connect a wallet and create your first stream on Stellar testnet. No setup, no custody, no
175-
code.
129+
{cta.description}
176130
</p>
177131
<div className="relative mt-8 flex justify-center">
178132
<Button asChild size="lg">
179133
<Link href="/app/create">
180-
Create a stream
134+
{cta.createStreamButton}
181135
<ArrowRight className="size-4" />
182136
</Link>
183137
</Button>
@@ -192,10 +146,7 @@ export function Footer() {
192146
<footer className="border-t border-border">
193147
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-4 px-4 py-8 sm:flex-row sm:px-6">
194148
<Brand />
195-
<p className="text-xs text-muted-foreground">
196-
{APP_NAME} streams tokens on Stellar. Demo frontend — connect your Soroban contracts to go
197-
live.
198-
</p>
149+
<p className="text-xs text-muted-foreground">{footer.tagline(APP_NAME)}</p>
199150
</div>
200151
</footer>
201152
)

‎components/layout/navbar.tsx‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ import { ConnectWalletButton } from '@/components/layout/connect-wallet-button'
1919
import { NotificationBell } from '@/components/layout/notification-bell'
2020
import { navigationCopy } from '@/lib/copy/navigation'
2121
import { cn } from '@/lib/utils'
22+
import { navbarCopy } from '@/lib/copy/navigation'
2223

2324
const NAV_LINKS = [
25+
{ href: '/app', label: navbarCopy.navLinks.dashboard },
26+
{ href: '/app/streams', label: navbarCopy.navLinks.streams },
27+
{ href: '/app/analytics', label: navbarCopy.navLinks.analytics },
28+
{ href: '/app/settings', label: navbarCopy.navLinks.settings },
2429
{ href: '/app', label: navigationCopy.links.dashboard },
2530
{ href: '/app/streams', label: navigationCopy.links.streams },
2631
{ href: '/app/analytics', label: navigationCopy.links.analytics },
@@ -64,6 +69,11 @@ export function Navbar() {
6469
{!isOnline && (
6570
<span
6671
role="status"
72+
title={navbarCopy.offlineTitle}
73+
className="flex items-center gap-1.5 rounded-full border border-amber-500/40 bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-600 dark:text-amber-400"
74+
>
75+
<WifiOff className="size-3.5" />
76+
<span className="hidden sm:inline">{navbarCopy.offlineLabel}</span>
6777
title={navigationCopy.offline.title}
6878
className="flex items-center gap-1.5 rounded-full border border-amber-500/40 bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-600 dark:text-amber-400"
6979
>
@@ -77,6 +87,10 @@ export function Navbar() {
7787
size="sm"
7888
className="gap-1.5"
7989
disabled
90+
aria-label={navbarCopy.newStreamDisabledAriaLabel}
91+
>
92+
<Plus className="size-4" />
93+
<span className="hidden sm:inline">{navbarCopy.newStreamButton}</span>
8094
aria-label={navigationCopy.newStream.unavailableAriaLabel}
8195
>
8296
<Plus className="size-4" />
@@ -86,6 +100,7 @@ export function Navbar() {
86100
<Button asChild variant="ghost" size="sm" className="gap-1.5">
87101
<Link href="/app/create">
88102
<Plus className="size-4" />
103+
<span className="hidden sm:inline">{navbarCopy.newStreamButton}</span>
89104
<span className="hidden sm:inline">{navigationCopy.newStream.label}</span>
90105
</Link>
91106
</Button>
@@ -96,6 +111,7 @@ export function Navbar() {
96111
<Button variant="ghost" size="sm" className="gap-1.5">
97112
<Network className="size-4" />
98113
<span className="hidden sm:inline">
114+
{network === 'mainnet' ? navbarCopy.network.mainnet : navbarCopy.network.testnet}
99115
{network === 'mainnet'
100116
? navigationCopy.network.mainnet
101117
: navigationCopy.network.testnet}
@@ -104,6 +120,10 @@ export function Navbar() {
104120
</DropdownMenuTrigger>
105121
<DropdownMenuContent align="end">
106122
<DropdownMenuItem onClick={() => setNetwork('testnet')}>
123+
{navbarCopy.network.testnet}
124+
</DropdownMenuItem>
125+
<DropdownMenuItem onClick={() => setNetwork('mainnet')}>
126+
{navbarCopy.network.mainnet}
107127
{navigationCopy.network.testnet}
108128
</DropdownMenuItem>
109129
<DropdownMenuItem onClick={() => setNetwork('mainnet')}>
@@ -117,12 +137,22 @@ export function Navbar() {
117137
<Button variant="ghost" size="icon">
118138
<Sun className="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
119139
<Moon className="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
140+
<span className="sr-only">{navbarCopy.theme.toggleSrLabel}</span>
120141
<span className="sr-only">{navigationCopy.theme.toggle}</span>
121142
</Button>
122143
</DropdownMenuTrigger>
123144
<DropdownMenuContent align="end">
124145
<DropdownMenuItem onClick={() => setTheme('light')}>
125146
<Sun className="mr-2 size-4" />
147+
{navbarCopy.theme.light}
148+
</DropdownMenuItem>
149+
<DropdownMenuItem onClick={() => setTheme('dark')}>
150+
<Moon className="mr-2 size-4" />
151+
{navbarCopy.theme.dark}
152+
</DropdownMenuItem>
153+
<DropdownMenuItem onClick={() => setTheme('system')}>
154+
<Monitor className="mr-2 size-4" />
155+
{navbarCopy.theme.system}
126156
{navigationCopy.theme.light}
127157
</DropdownMenuItem>
128158
<DropdownMenuItem onClick={() => setTheme('dark')}>
@@ -146,6 +176,9 @@ export function Navbar() {
146176
<div className="flex items-center gap-2 border-t border-yellow-500/30 bg-yellow-500/10 px-4 py-2 text-sm text-yellow-600 dark:text-yellow-400">
147177
<AlertTriangle className="size-4 shrink-0" />
148178
<span>
179+
{navbarCopy.networkMismatch.prefix} <strong>{walletNetwork}</strong>{' '}
180+
{navbarCopy.networkMismatch.middle} <strong>{network}</strong>{' '}
181+
{navbarCopy.networkMismatch.suffix}
149182
{navigationCopy.networkMismatch.before}
150183
<strong>{walletNetwork}</strong>
151184
{navigationCopy.networkMismatch.switchTo}

0 commit comments

Comments
 (0)