Aggressive cleanup of 359 exports across 153 files identified as
having zero importers outside their declaring module:
- 105 symbols deleted entirely (no internal uses either)
- 254 symbols un-exported (still referenced file-locally; dropped the
`export` keyword to shrink the public surface)
- ~70 cascade cleanups of locals that became dead once their sole
consumer was removed
Notable shrinkage:
- src/hooks/useShakespeare.ts: 626 \u2192 22 lines (unwired AI chat surface;
only the ChatMessage type is consumed)
- src/hooks/useTrending.ts: only useEventStats survives; trending feed
hooks were never wired up
- src/hooks/useTrustedCountryStats.ts: dead type re-exports removed
- src/lib/bitcoin.ts: PSBT helpers \u2014 unused wallet feature scaffolding
- src/lib/communityUtils.ts: unused NIP-72 moderation helpers
- src/lib/extraKinds.ts, src/lib/colorUtils.ts: unused helpers
- src/lib/logger.ts: bare debug/info/warn/error exports dropped;
consumers use the `logger` object
- src/lib/aiChatSystemPrompt.ts: trimmed to the
DEFAULT_SYSTEM_PROMPT_TEMPLATE constant
- src/components/music/MusicTrackRow.tsx: dead row component removed;
only the skeleton is consumed
src/hooks/useNostr.ts (intentional decoy) and src/i18n.ts
(side-effect import) were preserved per their respective contracts.
Extracts the ~285-line JSX prose body into per-language markdown files
served through the existing `usePolicyMarkdown` hook + `PolicyMarkdown`
component shipped with the privacy page.
Ships en/es/zh/ar/fa/ps. Khmer and Shona are left unregistered for now
and fall back to English at runtime via the loader's en fallback path —
the policy text is dense and translators will want a careful pass.
No new infrastructure; reuses the loader + renderer end-to-end.
Extracts the ~110-line JSX prose body into per-language markdown files
and renders it through a new `usePolicyMarkdown` hook + `PolicyMarkdown`
component. Eight locales ship beside `en.md`; missing locales fall back
to English at runtime.
The loader uses dynamic `import()` of `*.md?raw` (Vite + Bun friendly,
no `import.meta.glob`) keyed on a static `{ slug: { lng: loader } }`
table so bundlers can code-split per locale and we keep one shared chunk
for the markdown ecosystem. `{{appName}}` placeholders are interpolated
at render time with backslash-escape of markdown specials, and the
rendered output goes through rehype-sanitize.
This is the reusable loader infrastructure for the long-form policy
pages — CSAEPolicyPage is the next consumer.
Four files with zero importers across the repo:
- src/components/TeamSoapboxCard.tsx
- src/components/discovery/ProfileCard.tsx (superseded by
src/components/ProfileCard.tsx)
- src/components/letter/StickerPicker.tsx (superseded by
src/components/StickerPicker.tsx)
- src/hooks/useDominantColor.ts
src/hooks/useNostr.ts is also orphan but intentional \u2014 it exists as
a re-export decoy and is kept.
Drop 13 packages that have zero imports across src/, configs, and
native projects:
- @radix-ui/react-{menubar,navigation-menu,aspect-ratio,context-menu}
(no shadcn primitive consumes them)
- react-leaflet, leaflet, @types/leaflet (no map usage)
- smol-toml, fflate, html-to-image, input-otp
- react-resizable-panels, react-day-picker
Also drops the orphaned .leaflet-* CSS overrides in src/index.css.
The 11 cover images in /public/challenge-covers/ were superseded by
Blossom-hosted URLs in DEFAULT_ACTION_COVERS. No code referenced the
local files; the comment claiming otherwise was stale.
Reclaims ~4.4 MB from the bundle.
Adds the about.* namespace covering the entire /about landing page:
- hero — eyebrow ('About {appName}'), the 3-part headline with the
highlighter span (split into headlinePart1 / appName / headlinePart2
so each language can put the verb where it naturally belongs), body
paragraph, the three trust chips (Decentralized, Open source,
Censorship resistant), the two CTA buttons, and every string in the
tilted sample-campaign card (Venezuelan vigil alt text, org name,
campaign title, two-line description, 'raised / of $10,000',
'N donors · N countries' line, Donate Bitcoin button).
- howItWorks — section header + lede, plus three step cards (image alt
text, title, body) covering signup / send / spend.
- twoWays — section header, the two RailCards (kicker, tagline, title,
description, three bullets, tradeoff title + intro + bullets each),
the trade-off intro for Public Payments goes through <Trans /> with
inline links to the Donor and Activist guides, and the No-custody
banner including title, body, and the three GoFundMe / GiveSendGo /
'other Bitcoin' comparison items.
- faq — section eyebrow + title, and the three FAQ chapter labels +
descriptions (Getting started / Bitcoin donations / About Nostr).
FAQ_CHAPTERS stays a module-level constant; its label/description
fields just became labelKey/descriptionKey suffixes that AboutPage
hands to t() at render time.
- guides — section header + lede, both Donor and Activist guide cards
(image alt, role chip, title, description, three bullets each, cta),
and the closing 'Still stuck? Follow Team Soapbox' line.
The FAQ accordion bodies themselves come from helpContent.ts via
HelpFAQSection — that file (701 lines of prose) is the Stage E2
candidate and stays English here.
Added rtl:rotate-180 to the two hero CTA arrow icons and the guide-
card arrow icons so they flip in RTL.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Adds the policyPages.* namespace with five sub-namespaces, one per
page, all just translating the PageHeader title, SEO title/description,
and any chrome strings the page wraps around its (untranslated) body:
- privacy.* — PrivacyPolicyPage header. Body prose stays English for
E2 markdown extraction.
- csae.* — CSAEPolicyPage header. Same — body is policy prose.
- donorGuide.* — DonorGuidePage title + subtitle for the GuideHero.
Prose blocks come from helpContent.ts (still English; that file is
the E2 candidate).
- activistGuide.* — ActivistGuidePage hero same shape as donor.
- changelog.* — ChangelogPage chrome: title, error/empty states, Past
releases divider, Show less / Read more, pre-release banner ('Pre-
release build', body line, View unreleased changes link), and a
category-tooltip map (Added / Changed / Deprecated / Removed / Fixed
/ Security). The markdown body itself stays raw English — Read more
truncates whatever the parser produced.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Adds two large namespaces:
noteCard.* — the action labels surfaced under reaction / repost / poll
vote actor rows (reacted / reposted / voted), the Read more / Show less
truncation toggle, the live-stream status badges (LIVE / ENDED /
PLANNED / UNKNOWN), the Bot account avatar title, the donation prefix
on synthetic zap cards (Donated X to / Donated to), and the entire
KIND_HEADER_MAP — every 'X did a Y' header for the 24 kinds Agora
renders an action header for (photos, encrypted messages, letters,
treasures + finds, decks, emoji packs, groups, campaigns, badges,
streams, Zapstore app/release/asset, generic apps, git repos / patches
/ PRs, NIPs, nsites, zaps, pledges, follow packs, follow sets).
The KIND_HEADER_MAP refactor pulls the same trick as profileSettings
presets: each entry stores i18n keys instead of English strings, and
EventActionHeader runs them through t() at render time. publishedAtKey
is just publishedAtAction returning keys instead of phrases. The map
itself stays a module-level constant.
noteMoreMenu.* — the entire 'more' overflow menu hanging off every
NoteCard: every menu item (View post details, View Event JSON,
Bookmark, Add to list, Add/Remove from sidebar, Pin/Unpin to profile,
Pin/Unpin to country feed, Mute Conversation / mute @user / Report /
Remove from group / Delete post), the Encrypted content fallback in
the post preview, every toast (success + failure for each mutation),
the Delete confirmation alert dialog, and the Event JSON dialog (title,
Event ID label, Raw JSON label, Broadcast Event button + Broadcasting
state, plus the {{label}} copied toast).
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Adds the compose.* namespace covering ComposeBox: placeholders
(default, poll, country-scoped, content warning), preview / edit toggle,
poll mode (Add option, Option N, Single/Multiple choice, Back to post,
Publish poll, success/failure toasts), voice recording (Cancel, Send,
Sending, mic-denied toast, voice-message tooltip), toolbar
(Attach file, Emoji/GIF, More, Poll, Spoiler, Emoji, Stickers tabs),
submit (Posting / Posted toasts, upload-failed / publish-failed),
and destination dropdown (Post to label, Global / community
explainer popover, Choose another country picker, search placeholder,
empty state, Set as default, default-updated toasts).
Also adds replyModal.* for ReplyComposeModal — the six title fallbacks
(New poll, New comment, Comment on profile, Reply to post, Quote post,
New post), three placeholder fallbacks, and the Bluesky disclaimer.
ComposeBox's hardcoded default placeholder ('What's on your mind?') and
submitLabel ('Post!') props moved from prop defaults to t() fallbacks
inside the component so existing call sites can keep passing translated
strings unchanged. Renamed an inner .map((t) => …) variable in the poll
type/duration row to .map((pt) => …) since t shadowed the i18next t.
Added rtl:rotate-180 to the 'Back to post' chevron since it visually
points left/back.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Two issues left over from the previous restore:
- `ps-1` adds 0.25rem of padding-inline-start. In LTR that pushes
the first letter rightward off the box's left edge, creating a
visible gap. Drop to `ps-0` so the letter sits flush with the
start edge.
- Bebas Neue's italic skew shifts the visible left edge of "U"
rightward of its geometric box, leaving an apparent gap even with
zero padding. Apply `text-indent: -0.06em` to pull the letter back
into the box. The shift is small enough that other scripts
(Arabic, Khmer, Chinese) tolerate it.
Extends the feed.* namespace with five new sub-namespaces:
compose.placeholder, tabs (Follows / Following / Global), empty (the
six per-tab + per-mode empty-state messages plus the three CTA labels
that show under them), modeSwitcher (the home feed's mode-picker dropdown
with Agora / All Nostr / Following options, the dropdown trigger's aria
label, and the disabled-Following tooltip), replyContext (Replying to,
the and-joiner, plus a pluralized andOthers_one/andOthers_other), and
actions (Reply / Repost / Undo repost / React / Zap / Share / More + the
Link-copied toast).
PostActionBar's replyLabel prop default went from 'Reply' to undefined;
the component falls back to t('feed.actions.reply') at render time so
existing call sites that pass an already-translated string keep working
unchanged. FeedModeSwitcher's OPTIONS array swapped its hard-coded
label string for an i18nKey suffix, materialized through t() inside the
component.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Commit ab8457a8 migrated the home-page hero tagline to <Trans /> and
passed the wrapper elements via the OBJECT form of the `components`
prop:
<Trans components={{ 0: <span className="bg-primary ..." /> }} />
In our react-i18next version (17.0.4), the object form silently drops
the indexed tags — the translated text renders, but stripped of the
wrapping element, so the orange highlighter span never makes it into
the DOM.
Switching to the ARRAY form makes i18next pick up the indexed tags:
<Trans components={[ <span className="bg-primary ..." />, <br /> ]} />
While here:
- Add an index-1 <br /> so English keeps its original two-line layout
(Connecting activists to / unstoppable funding.). Translations that
prefer inline flow simply omit <1></1> from their string.
- Switch the highlighter's padding from `pl-1 pr-3` to logical
`ps-1 pe-3` so the asymmetric flourish extending past the word's
trailing edge flips correctly for RTL languages (ar, fa, ps).
Regression-of: ab8457a8
Adds the profileSettings.* namespace covering ProfileSettings:
PageHeader (title, subtitle, Save button), intro section, profile
fields (Website, Lightning, label/value/ticker/address placeholders),
the seven field-preset pills (Music, Photo, Video, Email, Wallet, Link,
Weather, Custom) with labels, descriptions, and value placeholders,
media mismatch warnings (6 keys covering audio / image / video × wrong
type / unknown extension), the upload tooltip, crop dialog titles,
mobile fields preview button, Advanced section + Bot Account row, save
toasts, and the BackupKeySection (Your Key heading, extension / bunker
body, secret-key explainer + warning, copy / reveal / hide aria labels,
Back Up Key button, all backup-related toasts).
Module-level structures pulled the same way as elsewhere this session:
FIELD_PRESETS / CUSTOM_PRESET became key-only skeletons
(FIELD_PRESET_SKELETONS / CUSTOM_PRESET_SKELETON), and a useFieldPresets
hook materializes them inside the component over t(). The locale-
independent fields (icon, type, accept, formatHint, defaultLabel
emojis) stay at module scope. getMediaMismatchWarning was renamed to
getMediaMismatchWarningKey and now returns a translation key suffix
instead of an English string — the SortableFieldRow translates the key
at render time.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
Adds the profile.* namespace covering ProfilePage (the more menu, follow
toast, image lightbox, follow/follower modals, Bitcoin QR modal, inline
profile fields, NIP-05 not-found states) and every sub-component the page
mounts: ProfileIdentityRail (action bar, stats, campaigns/pledges/groups
sections, RailPledgeCard, RailOrgCell), ProfileActivityTab,
ProfileCampaignsTab, ProfilePledgesTab, and OrganizationsAllDialog.
The desktop and mobile tab arrays (DESKTOP_TAB_LABELS / MOBILE_TAB_LABELS)
moved from English label strings to label keys so the tab maps are
translated through t('profile.tabs.*') at render time. CORE_TAB_IDS now
keys off the same identifier slugs to keep the click-through routing
intact.
Inner sub-components (RailCampaignsSection, RailPledgeCard, RailOrgCell,
StatList, ActionBar, ProfilePledgeCard, ProfileFieldInline,
BitcoinQRModal, FollowingListModal, FollowersListModal,
ProfileImageLightbox, ProfileMoreMenu) each pick up their own
useTranslation() per the established pattern — no t prop drilling.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn, ps,
fa still pending native-speaker review.
Adds a top-level search.* namespace covering the global search page:
- SEO + PageHeader title, three tab labels (Agora / Nostr / Accounts).
- The whole filters popover: title + Reset button, 'From' author-scope
segmented control (Anyone / Follows / People), 'Sort' segmented
control (Recent / Hot / Trending), the four selects (Media,
Protocol, Language, Kind), the custom-kind input placeholder, and
the 'Include replies' switch label.
- The active-filter chip strip (built in a useMemo) — 'No replies',
'Images', 'Videos', 'Shorts & Divines', 'No media', 'Mastodon',
'Bluesky', 'Hot', 'Trending', 'All kinds', 'Kind: {{kind}}',
'Kind {{kind}}', kindsCount_one / _other, 'My follows', and
authorsCount_one / _other.
- The Clear shortcut button, the 'search:' debug-string label, and
the 'New posts' pill with newPosts_one / _other plural.
- Five empty-state messages (Posts results / Posts prompt / Accounts
results / Follows prompt / Agora results / Agora prompt), plus the
EmptyState 'Active filters:' header and 'Clear all filters' button.
- The 'Following' badge title and 🤖 'Bot account' tooltip on
AccountItem + FollowItem (each needs its own useTranslation()).
- The SearchInput placeholder.
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Adds a top-level notifSettings.* namespace covering the
/settings/notifications page: SEO meta, page header (title +
subtitle), Push Notifications section (heading, enable-push row,
unsupported/denied banner copy), Android-only Delivery Method
section (Push / Persistent radio with descriptions), 'Notify Me
About' section with the Filter / Types sub-headings, 'Only from
people I follow' row, and the eight notification type rows
(reactions, reposts, zaps, mentions, comments, badges, letters,
highlights) — each with its own label + description under
notifSettings.types.{key}.
NOTIFICATION_TYPES rows now carry labelKey / descriptionKey
strings instead of literal English, looked up via t() inside the
component render. The enable-failure toast (title + description)
also runs through t().
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Adds a top-level notifications.* namespace covering the entire
notifications feed. Highlights:
- The 48-entry NOTIFICATION_KIND_NOUNS map (kind → bare noun used
in 'reacted to your <noun>' / 'reposted your <noun>' etc.) was
moved into notifications.kinds.* in the locale files and looked
up via a new useNotificationKindNoun() hook. The component-side
table now only maps kind → i18n key suffix so it stays small and
diffable when new kinds are added.
- Action-verb strings for every notification type are interpolated:
reactedToYour / repostedYour use {{noun}}; zappedYou has a
zappedYouWithAmount variant taking {{sats}}; commentedOnYour
takes {{noun}}; highlightedYour takes {{noun}}; mentionedYou,
repliedToYourNote, repliedToYourComment, sentYouLetter,
awardedBadge, and awardedBadges are flat.
- The condensed group subject uses <Trans i18nKey='subject.twoActors'
components={{0: ActorLink, 1: ActorLink}}> so component order can
flip in RTL languages; the 'and N others' branch uses
andOthers_one / _other and {{count}}.
- '+N more' actor-overflow counter uses interpolation.
- Letter notification's 'View all letters' / 'Reply' buttons,
SEO meta, tab labels (All / Mentions), the empty state and
logged-out CTA all routed through t().
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Adds a top-level spScan.* namespace covering the BIP-352 silent-
payment scan dialog: header (title + description), the from/to block
inputs (label + 'tip' placeholder), the indexer-tip / last-scanned
footer line (with 'never' fallback), the include-already-spent
toggle + its long explanation, the in-progress block-counter and
matches_one/_other plural, the post-scan summary
(scannedRange + foundOutputs_one/_other / noNewPayments), and the
'Reconcile spent UTXOs' subsection (title, long description,
checking interpolation, checked_one/_other plural with prune count,
Reconciling… / Reconcile now button). Action buttons reuse
common.cancel + common.close.
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Two top-level namespaces:
walletSend.* — the Send Bitcoin dialog at /wallet: dialog title,
amount approx-sats interpolation, recipient label/placeholder, the
three recipient descriptions (silent payment / Nostr / raw address),
fee speed labels (10 min / 30 min / 1 hour / 1 day) and the
{{rate}} sat/vB display, network fee row, available-balance footer,
the four progress strings (building / signing / broadcasting /
sending), the 10-error error catalogue thrown into setError() +
the mutation onError toast title, two-tap arming ('Tap again to
confirm'), and the SuccessScreen (title, sats fallback, view
transaction, done).
bitcoinPublic.* — the BitcoinPublicDisclaimer shared component
used by both the wallet's Send dialog and the campaign DonateDialog.
Lead sentence, 'Learn more' link, the two long body variants
(with/without cash-out advice), and the 'I understand this
transaction is public.' acknowledgement label. The leadText prop
override is preserved; only the default is now translated.
The FEE_SPEED_LABELS module constant was replaced by an in-component
useMemo so the labels stay reactive to language switches.
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Adds a top-level walletConnect.* namespace covering the NWC settings
component used by the Advanced settings page: status section (WebLN
+ NWC cards with Ready / Not Found / None badges), the Add button,
empty state, per-connection rows (active badge, defaultWalletName
fallback, set-active and remove icon-button titles), the help text
shown when no signing method is available, and the Connect-NWC
dialog (title, description, alias placeholder, Connect /
Connecting... button).
Toast titles+descriptions for URI-required validation and active-
wallet-changed feedback also go through t(). The connectedCount_one
/ _other plural lets each locale handle pluralisation natively (zh
collapses to one form, ar has separate one/other, etc.).
All 7 non-English locales drafted inline. Native-speaker review
still pending for km, sn, ps, fa.
Adds a top-level walletRecovery.* namespace covering the legacy
Breez/Spark Lightning sweep flow: SEO meta, page chrome, logged-out
empty state, destination address card, recovery-phrase input
(including the NIP-78 backup detection alert, the amber 'only paste
phrases you trust' warning, and the sweep button), the in-progress
loader with all five progress strings (loading SDK, connecting,
checking balance, preparing transfer with sats interpolation,
broadcasting), success card (sats interpolation + view-transaction),
error card, toast on successful relay-backup decrypt, and the six
walletRecovery.errors.* strings thrown into setError().
Back-to-wallet arrow flipped with rtl:rotate-180. All 7 non-English
locales drafted inline. Native-speaker review still pending for km,
sn, ps, fa.
Adds a top-level wallet.* namespace covering the Bitcoin wallet
page: SEO meta, logged-out / unsupported-signer empty states, balance
display (refresh + pending), Send / Receive buttons, the Receive
dialog (on-chain + silent-payment tabs incl. wsrv-style <0>...</0>
Trans interpolations for the bip352IndexerUrl mono span and silent
balance highlight), and the transactions list (relative date helper
now takes t + i18n.language).
formatTxDate was lifted from a closure constant to a parameterised
helper so 'Today' / 'Yesterday' / '{count}d ago' / 'Pending' route
through t() and toLocaleDateString uses the active i18n language
(with an en-US fallback if the locale is unknown to Intl).
All 7 non-English locales drafted inline. Native-speaker review still
pending for km, sn, ps, fa.
Conflicts in NetworkSettingsPage.tsx and SettingsPage.tsx, both
introduced by upstream's Low-Bandwidth Mode + image proxy feature
landing on top of my i18n refactor of the same settings pages.
Resolution:
- Translated the new Low-Bandwidth Mode and Image Proxy block under
settings.network.{lowBandwidthHeading,reduceDataUsage,
reduceDataUsageDesc,useImageProxy,useImageProxyDesc,proxyUrl,
proxyApiDesc,reset}, using <Trans> for the wsrv.nl link in
proxyApiDesc (<0>...</0> component slot).
- Updated settings.network.subtitle and settings.sections.networkDesc
in en.json to match the new upstream English ("Manage data usage,
relays, and file upload servers." / "Data usage, relays, and file
upload servers.").
- Mirrored the new keys + updated values into all 7 non-English
locales. Native-speaker review still pending for km, sn, ps, fa.
Extract user-facing strings on the campaign detail page into the
campaignsDetail.* namespace. Covers the hero (back/edit/delete chips,
author attribution, deadline pill, comment action label), the
engagement counter row above the comments (repost/quote/like counts
with pluralized labels and a bold count wrapper), the comments +
donations section header and empty state, the delete-confirm
AlertDialog, the donate sidebar (raised/of-goal labels, donation
count, recent-donations list, share button, ended state), the story
component, and pin/unpin and deletion toasts.
Chevron + arrow icons flip with rtl:rotate-180. Uses i18next plural
suffixes for the four count-driven labels (reposts/quotes/likes,
comments, donations, days-left).
This completes Priority 1 of the i18n rollout (Pledges +
Communities/Groups + Campaigns verticals — list pages, create forms,
detail pages).
Native-speaker review still pending for: km, sn, ps, fa.
Extract user-facing strings on the create-campaign form (and its edit
variant) into the campaignsCreate.* namespace. Covers the four gate
states (login, invalid edit link, loading, not-author), the wallet
picker (source dropdown, accept-types dropdown, custom address +
silent-payment inputs with inline validation), the field labels and
placeholders, the country selector, success/error toasts, and the
mutation's twenty user-facing error messages.
Back-arrow flips with rtl:rotate-180.
Native-speaker review still pending for: km, sn, ps, fa.
Extract user-facing strings on the create-group form (and its edit-mode
variant) into the groups.create.* namespace. Covers all four
non-success states (login gate, invalid edit link, loading-group
spinner, not-the-founder gate), the field labels and placeholders, the
URL preview footer with mono-spaced slug, moderator chip rows and
remove-button aria, the cover-image and country selectors, the submit
button's create/edit/uploading variants, and the mutation's eight
user-facing error messages.
Back-arrow flips with rtl:rotate-180.
Native-speaker review still pending for: km, sn, ps, fa.
Previously the tap-to-load placeholder only kicked in when the proxy was
also disabled. The proxy still saves bandwidth, but users who flipped on
low-bandwidth mode generally want the explicit consent step on every
image — proxied or not. Make the two settings independent:
- ImageGallery / ProxiedImage: gate on lowBandwidthMode alone.
- LinkPreview: suppress thumbnail whenever lowBandwidthMode is on.
- Settings copy + AppConfig JSDoc updated to match.
Extract user-facing strings on the groups index into the groups.list.*
namespace: hero copy, the moderator review rails (Needs review / Hidden),
the My-groups and Featured-groups shelves, the empty states for both
logged-out viewers and logged-in users with no groups, the ticker stat
labels, and the show-more/show-less collapsible toggle.
Uses i18next plural suffixes for the three ticker stats (campaigns
raised, featured groups, countries posting today).
Native-speaker review still pending for: km, sn, ps, fa.
Translate the pledge detail page chrome into the pledges.detail.*
namespace, including the hero (back button, deadline pill, author
attribution, share/submit actions), the funding sidebar (funded/of
amount/trust note/share button), the submissions section
(skeleton/empty state/composer placeholder), pin/unpin toasts, and
the loading variant rendered by NIP19Page while the addressable
coordinate decodes.
Uses i18next plural suffixes for submission counts and days-left
labels. Chevron + arrow icons flip with rtl:rotate-180.
Native-speaker review still pending for: km, sn, ps, fa.
Extract user-facing strings on the create-pledge form into the
pledges.create.* namespace and add per-form-section labels via a new
forms.* namespace shared with FormSection (Required/Recommended/
Optional badge).
Translates the login gate, field labels, placeholders, the
mutation's user-facing error messages, success / failure toasts, the
country search box, and the country-hint footnote (with a <0> wrapper
around the iso3166 monospace span). The back-arrow flips with
rtl:rotate-180 for Arabic/Farsi/Pashto.
Native-speaker review still pending for: km, sn, ps, fa.
Two new AppConfig fields cover the data-saving story:
- `imageProxy` (default `https://wsrv.nl`) — wsrv.nl/weserv-compatible
image-resizing proxy. Empty string disables it. `proxyImageUrl(src, width)`
and `useImageProxy()` rewrite URLs to WebP at quality 75; the `default=`
param redirects to the origin if the proxy can't fetch upstream. The
proxy base URL is parsed through `URL` and rejected unless it's
`https:`, so user input from the settings field can't smuggle non-https
schemes into <img src>.
- `lowBandwidthMode` (default `false`) — forces autoplay off everywhere
(VideoPlayer, LiveStreamPlayer), skips `useVideoThumbnail`'s background
frame-grab, and (when the proxy is also disabled) gates feed images and
link-preview thumbnails behind a tap-to-load placeholder.
The two settings are independent. A privacy-conscious user can run with
the proxy off without being forced into tap-to-load, and a metered-data
user can keep the proxy on without ever seeing a placeholder. Tap-to-load
only kicks in when both "I'm low-bandwidth" and "the proxy is off"
are true (or the proxy errors in a gated context).
Three new shared pieces:
- `src/lib/proxyImageUrl.ts` — pure URL rewriter
- `src/hooks/useImageProxy.ts` — memoized `(src, width) => string`
- `src/components/MediaPlaceholder.tsx` — tap-to-load pill with
optional blurhash background; rendered as `<div role="button">`
so it nests cleanly inside InlineImage's outer lightbox button
- `src/components/ProxiedImage.tsx` — `<img>` with proxy + onError
fallback + optional placeholder gating
Wired call sites with per-context widths:
Inline post images (InlineImage, w=600, gated)
Image gallery tiles (GridImage, w=600, gated)
Lightbox (LightboxImage, w=1200)
Profile banner (ProfileBannerImage, w=1200)
Profile hover banner (ProfileHoverCard, w=400)
Link preview thumb (LinkPreview, w=400, suppressed when
low-bandwidth + no proxy)
Sidebar media tile (ProfileRightSidebar, w=300)
Avatars (via shadcn) (AvatarImage, w=96 default,
w=128 hover card,
w=256 profile header)
Custom emojis (CustomEmojiImg, w=48)
Badge thumbnails (BadgeThumbnail, w=max(size*2, 128))
Badge hero + glare mask (BadgeContent, w=256;
BadgeDetailContent, w=320;
EmbeddedNaddr, w=192)
Music / podcast art (AudioKindContent, w=600;
MusicTrackRow, w=96;
MusicDetailContent, w=320 hero / 96 row;
PodcastDetailContent, w=320)
Settings UI lives in NetworkSettingsPage with the Low-Bandwidth Mode
toggle at the top (above relay / Blossom plumbing) and the Image Proxy
section between Blossom Servers and Image Uploads. The low-bandwidth
description text adapts to whether the proxy is configured, so the
proxy↔tap-to-load coupling is visible at the toggle.
Video Tier 1:
- VideoPlayer overrides `autoPlay` to false in low-bandwidth
- LiveStreamPlayer no longer hardcodes `autoPlay`, reads config
- useVideoThumbnail skips its background frame-grab entirely
`lowBandwidthMode` is added to EncryptedSettingsSchema so it crosses
devices alongside `autoplayVideos`. `imageProxy` is local-only (privacy
/ network choice, not a UX preference).
Extract all user-facing strings on the Pledges index page into the
pledges.* namespace and supply translations for the 7 non-English
locales. Includes the hero copy, the card chrome (Pledged label,
ended badge, share menu, attribution), the sort/filter controls, the
section dividers, the empty state, and the SEO title/description.
Uses i18next plural suffixes (_one/_other) for the open-count pill and
the show-more button uses {{count}} interpolation.
Native-speaker review still pending for: km, sn, ps, fa.
Add campaigns.home.* namespace covering the hero (tagline, body,
three CTAs), the Featured and Community Campaigns section headings and
descriptions, the Browse-all link, the moderator-only Pending and
Hidden section labels (title/description/emptyText), the non-mod
"Your campaigns" section, and the empty-state card.
The hero tagline uses <Trans i18nKey="campaigns.home.heroTagline"
components={{0: <span/>}}> so each locale controls where the orange
highlighter falls. The English-only inner-span optical alignment
(negative margin to counter Bebas Neue italic skew on a leading 'u')
is dropped — it was overfit to one word and would have produced ugly
results in any non-English layout.
The ArrowRight icon in the "How it works" CTA picks up rtl:rotate-180
so it points the right way in RTL languages.
Add feed.indexTagline, notFound.*, and campaigns.all.* namespaces. Wire
useTranslation into:
- Index.tsx: SEO description tagline ("Your content. Your vibe. Your
rules.")
- NotFound.tsx: 404 heading + "Go home" button + SEO meta
- AllCampaignsPage.tsx: page title, SEO meta, search aria/placeholder,
clear-search aria, sort radio labels (Top/New) + sort group aria,
show-hidden switch, start-campaign CTA, all three empty states (no
match, all hidden, empty) + their hints
CampaignsPage landing page still needs translation; deferred to next
commit because it has hero sections and featured-slot copy that's worth
a dedicated pass.
Add settings.appearance.*, settings.network.*, settings.advanced.*, and
settings.wallet.* namespaces. Wire useTranslation into the four small
settings sub-pages so the page header, intro text, theme picker labels,
relay/Blossom section headings, image upload quality toggle, and
collapsible Wallet trigger all switch language live.
Leaves the Notification, Profile, and Organizers settings sub-pages
plus the embedded WalletSettings / RelayListManager / BlossomSettings /
AdvancedSettings component bodies for a follow-up commit.
Wire useTranslation into TopNav (nav items, mobile drawer, profile menu,
search button aria-label, brand home aria-label, open/close menu labels,
mobile footer links) and SiteFooter (tagline + footer nav links).
Add nav.* namespace to en.json + all 7 locale files. The mobile drawer
and desktop nav now switch language live the moment a user picks a new
language in /settings/language.
Phase 1 multi-language foundation:
- Register 8 locales in i18next (en, es, ar, fa, ps, km, sn, zh) with
static-bundled JSON. Wire languageChanged listener to set
document.documentElement.lang and dir for RTL support.
- Expose SUPPORTED_LANGUAGES (code + nativeName) so the switcher and
validator pick up new entries automatically.
- Restructure en.json: drop ~280 dead wallet-port keys, introduce
settings.sections.*, language.*, and a tightened common.* namespace.
Keep organizers.* and the keys actually consumed by t() calls today.
- Translate common, settings, language, and organizers namespaces in
all 7 non-English locales (machine drafts; km/sn/ps/fa flagged for
native-speaker QA before announcement).
- Add LanguageSettingsPage at /settings/language with a radio-style
picker rendering each language's native name in its own script/dir.
- Add Language entry to settingsSections, between Appearance and Network.
- Translate SettingsPage strings (page title, section labels, delete
button) using the new keys. Flip the chevron with rtl:rotate-180.
- Add src/test/locales.test.ts validator that fails CI on any locale
introducing keys absent from en.json (typo / stale-translation guard).
No visible UX change in English. Switching language now translates the
Settings hub, the Language sub-page, and the existing Organizers admin
end to end.
The shared HorizontalScroll component was removed from main during
orphaned component cleanup. Instead of restoring the shared file,
inline the small helper directly in MyDashboardPage since it is the
only consumer.
Agora's colors and fonts are now hardcoded in the bundle. There is no
runtime CSS-variable injection, no remote-loaded theme, no font-family
override from event data, no background image, no recolored favicon.
Switching themes only toggles the .dark class on <html>.
Removed:
- src/themes.ts: ThemeConfig, ThemesConfig, CoreThemeColors, ThemeFont,
ThemeBackground, themePresets (22 presets), buildThemeCssFromCore,
deriveTokensFromCore, the 'custom' Theme variant. Now exports only
resolveTheme(theme) -> 'light' | 'dark'.
- src/lib/fontLoader.ts: deleted. Mounted arbitrary @font-face rules with
event-sourced URLs and font-family overrides — the primary CSS-injection
vector. sanitizeCssString moved to src/lib/cssSanitize.ts (still used by
the Letter feature).
- AppProvider hooks useApplyFonts / useApplyBackground / useApplyFavicon.
- useTheme.applyCustomTheme — the entrypoint that wrote external palettes
to global theme state.
- ColorMomentEyeButton + its callers in NoteCard and PostDetailPage. Color
Moments (kind 3367) still render as palette art, but the 'Set as theme'
button is gone.
- LetterAttachment in LetterDetailSheet — the gift-box UI that applied an
attached color moment as the recipient's theme.
- paletteToTheme() from colorMomentUtils — only getColors() remains.
- customTheme / themes fields from AppConfig, AppConfigSchema,
EncryptedSettings, EncryptedSettingsSchema. Existing customTheme values
in localStorage and encrypted NIP-78 settings are now ignored.
- 14 unused @fontsource packages. Only the 10 letter fonts and the 2 base
UI fonts (Inter Variable, Bebas Neue) remain. noto-sans-nushu is kept
for the encrypted-letter obfuscation indicator.
Added:
- Static :root {} and .dark {} blocks in src/index.css with the 19 shadcn
tokens hardcoded. These were previously injected at runtime by
AppProvider; without them the app would lose all colors when the runtime
injector was removed.
- src/lib/cssSanitize.ts holding the sanitizeCssString helper for the
Letter feature's font-family interpolation.
Simplified:
- public/theme.js: no longer reads customTheme or themes from localStorage,
just resolves system/light/dark with hardcoded built-ins. Must stay in
sync with src/index.css colors.
- src/lib/fonts.ts: trimmed to only the 10 letter fonts. findBundledFont
and resolveCssFamily removed (they were only used by fontLoader).
- Rename MySquarePage -> MyDashboardPage (file, component, default export)
- Change route from /my-square to /my-dashboard
- Update user-facing copy: SEO title, logged-out heading, JSDoc
- Add 'My Dashboard' link to AccountSwitcher dropdown
- Add 'My Dashboard' entry to TopNav mobile/profile menu
- Use LayoutDashboard icon to distinguish from the existing Dashboard
- Existing /dashboard route and EventDashboardPage are untouched
Replaces the previous near-uniform dark surfaces (#0e1218 / #11151c)
with genuinely dark, alternating tones plus a subtle world-map
texture on two sections, so the page reads as an editorial document
in dark mode the same way it does in light mode.
Section background mapping (dark mode)
Section Was Now
----------------------- ---------- ------------
Hero #0a0c14 #0a0c14 (unchanged)
Three steps #11151c #0a0c14 + texture
Two ways to get paid #0e1218 #13181f + texture
Frequently asked #11151c #0a0c14
Pick the side #0e1218 #13181f
The two alternating tones (#0a0c14 / #13181f) preserve the
cream/white rhythm from light mode. Sections 2 and 3 gain a
dark-only world-map background image at 5-6% opacity (the same
texture used in the hero) so they don't read as flat slabs. The
texture is gated behind 'hidden dark:block' so it has zero impact
on the light-mode rendering.
Card surface lifted
bg-[#1a1f29] → bg-[#1c2230] across StepCard, RailCard, GuideCard,
the FAQ accordion card-row (reference mode), the FAQ card variant,
and the FAQ tab pill. Hover-state for the tab pill follows from
#222937 to #252b3a. Net effect: cards now sit ~12 lightness above
the deepest section background and ~6 above the lifted one, keeping
clear elevation against both tones.
Every section of the About page now renders correctly in dark mode.
The hero stays dark in both modes (its identity is anchored on the
dark navy backdrop), and the four light sections (Three steps, Two
ways to get paid, Frequently asked, Pick the side) each gain a
dark-mode counterpart so the page reads consistently inside the
app's theme.
Section background mapping
Section Light bg Dark bg
----------------------- ----------- -----------
Hero #0a0c14 #0a0c14 (unchanged)
Three steps #faf8f4 #11151c
Two ways to get paid white #0e1218
Frequently asked #f5f1eb #11151c
Pick the side white #0e1218
Card surfaces (StepCard, RailCard, GuideCard, FAQ accordion item)
map bg-white -> dark:bg-[#1a1f29] and border-gray-200 ->
dark:border-white/10. Hover shadows pick up a darker variant
(dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.4)]) on cards that have
a hover-lift effect.
Text-color mapping (applied throughout)
text-gray-900 -> dark:text-white (headings)
text-gray-800 -> dark:text-gray-100 (inline strongs)
text-gray-700 -> dark:text-gray-300 (bullet body)
text-gray-600 -> dark:text-gray-300 (body prose)
text-amber-600 / 700 -> dark:text-amber-400 (trade-off label
+ icon)
border-gray-100 -> dark:border-white/10 (separators)
No-Custody comparison banner
The light gradient card (white -> primary/5) converts to a glass
tile in dark mode (white/0.04 -> primary/0.08) with primary/30
border. The inner Comparison grid's separator border picks up
dark:border-white/10. ComparisonItem's light theme path now also
sets dark:text-gray-300 on its body copy so it reads on the dark
glass tile.
HelpFAQSection
Both the 'reference' card-row accordion (used by the About page)
and the 'cards' masonry variant gain dark-mode card surfaces and
text-color mapping. The category tab pills get a dark-mode
inactive state. The active pill (bg-primary) is already correct in
both modes.
These three embed components rendered third-party content in
iframes (Twitter's platform.twitter.com widget, Spotify's
embed-iframe, Reddit's embed.reddit.com widget). Removed wholesale:
- SpotifyEmbed, TweetEmbed, RedditEmbed components.
- extractTweetId, extractSpotifyEmbed, extractRedditPost, and the
SpotifyEmbedInfo type from lib/linkEmbed.ts.
- The Twitter/Spotify/Reddit branches in LinkEmbed and
isEmbeddableUrl/embedLabel.
Tweet, Spotify, and Reddit URLs now fall through to the generic
LinkPreview card. useLinkPreview keeps its native Spotify/Reddit
oEmbed shortcuts so those previews still render with rich title +
thumbnail metadata instead of going through the link-preview proxy.