Derived from the existing Simplified Chinese (zh) locale via OpenCC-style
s2twp conversion (Simplified -> Traditional with Taiwan phrase
preferences), then hand-corrected for the cases where automated
conversion picks the wrong vocabulary swap:
- 支援 only for technical 'X supports Y feature' contexts; 支持 retained
for 'support a cause/person/activist' which is the more common sense
for an activism platform.
- 連結 (link/connect) rather than 連線 (online) in the hero tagline
'connecting activists to unstoppable funds'.
- 代碼 rather than 程式碼 for silent-payment / donation 'code'
identifiers (the source is a payment-address token, not source code).
- 實例 rather than 例項 for 'instance' (self-hosted weserv).
- 影片 rather than 視頻 for 'video' (Taiwan vocab).
- Fixed an automated-conversion artifact 聚整合員 -> 聚集成員.
The Traditional resource is also registered under zh-TW and zh-HK so
that browsers reporting those device locales route directly to the
Traditional file instead of falling back to Simplified zh.
Mainland zh-CN continues to resolve to zh (Simplified) via i18next's
nonExplicitSupportedLngs. The language switcher dropdown shows both
'简体中文' and '繁體中文' as distinct choices.
Key parity verified: 1677 leaves, same as en.json and zh.json, zero
missing or extra keys, all {{placeholders}} and <N>markup</N> tokens
preserved verbatim from the source.
Bebas Neue (the .font-display family) ships only Latin glyphs, so
Chinese hero headlines fall back to system fonts and lose their
industrial display character. Add @fontsource/noto-sans-tc weight 900
and a :lang() rule that swaps it in for any .font-display element
while the page language is a Chinese variant (zh, zh-Hant, zh-TW,
zh-HK).
The fontsource CSS uses unicode-range descriptors, so non-Chinese
users do not download the Han glyph slices (effectively zero cost
for Latin-only locales).
The rule reverses Tailwind's italic, uppercase, tracking, and the
hero's 0.022em -webkit-text-stroke fatten trick — none of those are
meaningful for CJK text and the stroke trick muddies strokes at
weight 900.
Adds a complete Türkçe translation. Turkish is spoken by ~90M people
and has an unusually strong mission fit for Agora: Turkey has
documented Bitcoin demand from currency-crisis users, journalist
crackdowns, and an active diaspora that often funds back home.
Register is modern İstanbul Turkish (BBC Türkçe / WhatsApp Türkçe
style) — polite-but-direct, formal siz throughout. English loanwords
kept where they're standard in everyday Turkish (Bitcoin, Lightning,
Nostr, röle, zap); Turkish equivalents used where they read naturally
(cüzdan, gönderi, anlık bildirim).
{{appName}} suffixing uses the standard Turkish apostrophe convention
for proper nouns ({{appName}}'da, {{appName}}'nın, etc.). Suffixes
are pinned to back-vowel harmony because "Agora" ends in 'a' — this
will break if AppConfig.appName is ever changed to a front-vowel word
like "Eylem" or "Yardım", but that's a deliberate trade-off for
not having to fork the JSON across deployments.
heroTagline mirrors the English design rhythm — line break before the
highlight, with the inline-block orange box on its own line as the
visual centerpiece:
"Aktivistleri<1></1><0>durdurulamaz</0> finansmana bağlıyoruz."
The trailing period is glued to a multi-word phrase rather than
sitting alone after the inline-block, avoiding the wrap bug we fixed
for Portuguese.
Turkish casing note: the hero h1 has text-transform: uppercase. Turkish
has dotted/dotless i (İ/i, I/ı). CSS uppercase honors the lang="tr"
attribute that applyDocumentDirection sets on <html>, so dotted
lowercase "i" uppercases to "İ" correctly without manual handling.
i18n auto-detection picks up the new locale on devices set to Turkish
(navigator.language returns tr/tr-TR).
Validation: en/tr leaf counts both 1677, no extra/missing keys,
locales.test.ts passes (120 tests, +2 for the new locale).
Adds a complete Bahasa Indonesia translation. Indonesian is the
eleventh-most-spoken language worldwide (~200M speakers) and brings
Agora to Indonesia's growing Bitcoin-aware activist community plus the
broader maritime Southeast Asia.
Register is standard Bahasa Indonesia (formal-but-conversational,
the style used by Kompas, Tempo, Tirto). Avoids Malaysian-Malay-only
vocabulary so Malaysian users (handled via nonExplicitSupportedLngs
folding ms-* → id is NOT happening here; ms users will fall back to
English until we add ms separately, which is acceptable given the
~85% mutual intelligibility). Uses Anda for the second person, the
established UI convention. Standard Indonesian tech vocabulary where
present (dompet, kampanye, donasi, donatur, ikrar, penggalangan dana,
pengaturan); English loanwords kept where they're normal in modern
Indonesian software (Bitcoin, Lightning, Nostr, feed, post, relay,
zap, NIP, BIP).
heroTagline uses the period-inside-highlight pattern:
"Menghubungkan aktivis dengan<1></1>pendanaan <0>tak terbendung.</0>"
The trailing period lives inside the orange highlight span, so it
can't orphan to its own line at the hero's text-8xl size — the same
fix we applied for Portuguese.
i18n auto-detection picks up the new locale on devices set to
Indonesian (navigator.language returns id/id-ID).
Validation: en/id leaf counts both 1677, no extra/missing keys,
locales.test.ts passes (118 tests, +2 for the new locale).
Adds a complete Hindi translation alongside the existing 11 locales.
Hindi (हिन्दी) is the fourth-most-spoken language in the world (~610M
speakers) and brings Agora to a large activist population currently
served only by the English fallback.
Translation register is Hindustani / news-style (BBC Hindi-flavored),
not Sanskrit-leaning शुद्ध हिन्दी — chosen for accessibility to the
broadest Hindi-speaking audience, including users who'd otherwise
struggle with formal government Hindi. Uses आप throughout for the
second person. Standard tech loanwords kept transliterated where they
read naturally (वॉलेट, पोस्ट, कैंपेन, फ़ीड); brand names and
protocol tokens (Bitcoin, Nostr, BIP-352, nsec, sp1…) kept in Latin
script.
heroTagline uses the safe structure — short highlight on the adjective
"अजेय" with a trailing phrase ending in danda (।), avoiding the
inline-block-followed-by-lone-punctuation wrap bug that previously hit
the Portuguese locale.
i18n auto-detection picks up the new locale on devices set to Hindi
(navigator.language returns hi/hi-IN), with nonExplicitSupportedLngs
folding regional variants into the hi bucket. The runtime fallback to
English still works for any keys we might miss in future edits.
Validation: en/hi leaf counts both 1677, no extra/missing keys,
locales.test.ts passes (116 tests, +2 for the new locale).
The Portuguese heroTagline read
"Conectando ativistas a<1></1><0>financiamento incontrolável</0>."
where the period sits outside the <0>...</0> orange highlighter span.
That span is rendered as inline-block w-fit (CampaignsPage.tsx:203-207),
and the boundary between an inline-block and the following bare text
node is a soft-wrap opportunity. When the highlighted phrase
"financiamento incontrolável" filled the line — which it does at the
hero's text-7xl/8xl sizes inside max-w-2xl — the trailing period
wrapped to its own line.
Restructure the markup so only the adjective is highlighted and the
period lives inside the orange box, mirroring the design intent of the
English original (where the highlight is on the adjective and the noun
trails it):
"Conectando ativistas a<1></1>financiamento <0>incontrolável.</0>"
"financiamento " is now plain text before the box, and the period is
part of the box's content, so there is no wrap opportunity between the
last visible character and the period.
Other locales (fr, ru) have the same structural shape but short enough
highlighted words that the issue doesn't manifest in practice; leaving
them alone.
The donation address derivation FAQ said "derived from your Nostr public
key," but the HD wallet (src/lib/hdwallet/derivation.ts) actually uses
the user's nsec (secret key) as the BIP-32 master seed for both the
BIP-86 Taproot wallet and the BIP-352 silent-payments wallet. Updated
the wording across why-not-rotating-addresses, what-is-nostr, the donor
guide's arrivesDirectly step, and the activist guide's howReceiving
intro.
The 'Does Agora support silent payments?' answer only mentioned the
activist receive side. Agora's own wallet supports sending silent
payments too (HDSendBitcoinDialog accepts sp1… addresses via the
BIP-352 sender in src/lib/hdwallet/sp/sender.ts), so the answer now
leads with that on the send side and follows with the existing
activist-receive paragraphs.
Propagated to all 10 non-English locales (ar, es, fa, fr, km, ps, pt,
ru, sn, zh).
Catches the ru/fr/pt locales up with the keys added upstream while the
initial translations were in flight: translate, forms additions,
organizationContext, groups.detail, calendarEvents, follow,
campaignsDetail.openInWallet, the assorted countryPlaceholder /
showLess / readMore entries, and a handful of scattered new keys.
Adds src/content/privacy/{ru,fr,pt}.md and src/content/csae/{ru,fr,pt}.md,
and registers them in usePolicyMarkdown.ts so the policy pages serve the
matching translation when the user's language is set to one of the three
new locales (falling back to en otherwise).
Bootstraps src/locales/{ru,fr,pt}.json from en.json as a starting
point and wires all three into i18n.ts. Strings are still English
pending the per-section translation passes that follow.
Fills the last two gaps in CSAE policy coverage. Khmer and Shona
markdown files land beside the existing six (en, es, zh, ar, fa, ps),
and both entries are registered in the usePolicyMarkdown loader.
Every long-form policy / guide surface is now translated in all eight
shipped locales.
Splits the guide content same way the FAQ was split:
* Structure (block order, kinds, audience, callout variant, option
grid chips/hrefs) stays in helpContent.ts as a typed array of
GuideBlockStructure descriptors with stable IDs.
* Every user-visible string moves to en.json under guides.donor.*,
guides.activist.*, and guides.shared.* (badge labels, tldr eyebrow,
payment comparison table headers + rows).
PaymentComparisonTable, InlinePaymentBadge, and GuideTLDR call
useTranslation() so a language switch triggers re-render. DonorGuidePage
and ActivistGuidePage already do, so getDonorGuideBlocks /
getActivistGuideBlocks re-run on every render and pick up fresh i18n
values automatically.
This commit lands the en strings only; non-en locales follow in the
next commit.
Ships the `faq.*` namespace in every non-English locale. Each locale
gets all 4 category labels and all 23 items (15 visible across About
Agora / Bitcoin Donations / About Nostr + 8 hidden legacy items used by
`HelpTip` call sites in settings pages).
Missing-key fallback to English still works via i18next's default
fallback chain — if a future FAQ item lands in en.json before the other
locales catch up, those locales will fall back to English at that key
without breaking the renderer.
Splits the FAQ definition in `helpContent.ts` into two layers:
* a structural template (category order, item IDs, hidden flag) that
stays in TS, and
* a flat `faq.*` namespace in `locales/*.json` that holds every
user-visible string (category labels, questions, answer paragraph
arrays).
`getFAQCategories` / `getFAQItems` / `getFAQItem` keep the same
`(appName)` signature and `FAQCategory` / `FAQItem` shape — internally
they resolve strings through `i18n.t()`, with `{appName}` literals
rewritten to `{{appName}}` for i18next interpolation. Answer arrays use
`returnObjects: true` so a missing locale entry falls back to English
without breaking the renderer.
`HelpFAQSection` reads `i18n.language` to re-resolve on language switch;
`HelpTip` calls `useTranslation()` for the same reason. The Donor /
Activist guide blocks below are still keyed off the original
single-brace `{appName}` literal — that's a separate i18n pass.
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.