Skip to content

Commit e2ec8e2

Browse files
sktbrdclaude
andauthored
feat(treasury): show fiat in R$ on the pt-BR locale (#274)
Every USD figure on the treasury page — total value KPI, allocation bar, token holdings values, sponsorship TVL/yield — converts to BRL on pt-br at one server-cached USD→BRL rate (open.er-api.com, 1h unstable_cache; errors are not cached so a miss retries next request). Token quantities never convert: a USDC position's USD value multiplies by the rate like everything else — 1 USDC is not R$ 1. If the rate fetch fails the page keeps US$ figures and each affected card says so out loud (FiatFallbackNote) instead of silently showing stale reais. EN is untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e664e79 commit e2ec8e2

13 files changed

Lines changed: 246 additions & 25 deletions

File tree

messages/en/treasury.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"description": "Non-fungible tokens held in the treasury"
1313
}
1414
},
15+
"fiat": {
16+
"usdFallback": "BRL rate unavailable — values shown in US$"
17+
},
1518
"tokens": {
1619
"title": "Token Holdings",
1720
"description": "ERC-20 tokens held in the treasury",

messages/pt-br/treasury.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"description": "Tokens não-fungíveis mantidos no tesouro"
1313
}
1414
},
15+
"fiat": {
16+
"usdFallback": "Cotação do dólar indisponível — valores em US$"
17+
},
1518
"tokens": {
1619
"title": "Holdings de Tokens",
1720
"description": "Tokens ERC-20 mantidos no tesouro",

src/app/[locale]/treasury/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TreasuryBalance } from "@/components/treasury/TreasuryBalance";
1515
import { TreasuryInflows } from "@/components/treasury/TreasuryInflows";
1616
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1717
import { DAO_ADDRESSES } from "@/lib/config";
18+
import { getBrlRateForRequest } from "@/services/exchange-rate";
1819

1920
export async function generateMetadata({
2021
params,
@@ -71,6 +72,9 @@ export default async function TreasuryPage({ params }: { params: Promise<{ local
7172
const { locale } = await params;
7273
setRequestLocale(locale);
7374
const t = await getTranslations("treasury");
75+
// One USD→BRL lookup per request, shared by every fiat display on the page.
76+
// SponsorshipYield is a client component, so its copy arrives as a prop.
77+
const brlRate = await getBrlRateForRequest();
7478

7579
return (
7680
<div className="py-8">
@@ -143,7 +147,7 @@ export default async function TreasuryPage({ params }: { params: Promise<{ local
143147
that void, and surrendering its old half-width row gives the NFT
144148
grid the full width it actually benefits from. */}
145149
<div className="space-y-6">
146-
<SponsorshipYield />
150+
<SponsorshipYield brlRate={brlRate} />
147151
<Suspense fallback={<TableSkeleton />}>
148152
<TokenHoldings treasuryAddress={DAO_ADDRESSES.treasury} />
149153
</Suspense>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { useLocale, useTranslations } from "next-intl";
4+
import { cn } from "@/lib/utils";
5+
6+
/**
7+
* Shown on pt-br whenever the USD→BRL rate could not be fetched: the page then
8+
* keeps its figures in US$, and this note says so out loud. Silent fallback is
9+
* the failure mode this exists to prevent — a visitor must never read a USD
10+
* number as reais.
11+
*/
12+
export function FiatFallbackNote({
13+
brlRate,
14+
className,
15+
}: {
16+
brlRate: number | null;
17+
className?: string;
18+
}) {
19+
const locale = useLocale();
20+
const t = useTranslations("treasury.fiat");
21+
if (locale !== "pt-br" || brlRate != null) return null;
22+
return (
23+
<p className={cn("text-xs text-amber-600 dark:text-amber-500", className)}>
24+
{t("usdFallback")}
25+
</p>
26+
);
27+
}

src/components/treasury/SponsorshipYield.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import { Button } from "@/components/ui/button";
3434
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3535
import { useStakeGraphQuery } from "@/hooks/use-stake-graph";
3636
import { RIDER_LIST, type RiderId } from "@/lib/gnars-vaults";
37+
import { localizeFiat } from "@/lib/i18n/fiat";
38+
import { toIntlLocale } from "@/lib/i18n/format";
39+
import { FiatFallbackNote } from "./FiatFallbackNote";
3740

3841
/**
3942
* Avatar crop per rider. Duplicated from StakeOrbit's `RIDER_VISUAL` on purpose,
@@ -57,7 +60,7 @@ export const AVATAR: Record<RiderId, { src: string; size: string; pos: string }>
5760
// the explorer renders the contract, its recipients and the Distribute button.
5861
const SPLITS_APP = (split: string) => `https://explorer.splits.org/accounts/${split}/?chainId=8453`;
5962

60-
export function SponsorshipYield() {
63+
export function SponsorshipYield({ brlRate = null }: { brlRate?: number | null }) {
6164
const t = useTranslations("treasury.sponsorship");
6265
const tc = useTranslations("stake.characters");
6366
const locale = useLocale();
@@ -66,8 +69,17 @@ export function SponsorshipYield() {
6669
// Nothing deployed yet — don't show an empty widget on the treasury page.
6770
if (RIDER_LIST.every((r) => !r.vault)) return null;
6871

69-
const usd = (n: number) =>
70-
`$${n.toLocaleString(locale, { maximumFractionDigits: n > 0 && n < 100 ? 4 : 2 })}`;
72+
const usd = (n: number) => {
73+
const { value, currency } = localizeFiat(n, locale, brlRate);
74+
// The 4-digit rule keys off the DISPLAYED magnitude: a sub-cent accrual
75+
// stays sub-cent after conversion and still needs the extra precision.
76+
return new Intl.NumberFormat(toIntlLocale(locale), {
77+
style: "currency",
78+
currency,
79+
minimumFractionDigits: 0,
80+
maximumFractionDigits: value > 0 && value < 100 ? 4 : 2,
81+
}).format(value);
82+
};
7183

7284
const byId = new Map((graph?.athletes ?? []).map((a) => [a.id, a]));
7385
const rows = RIDER_LIST.map((r) => {
@@ -109,6 +121,7 @@ export function SponsorshipYield() {
109121
<p className="mt-2 text-[11px] leading-relaxed text-muted-foreground/70">
110122
{t("accrualNote")}
111123
</p>
124+
<FiatFallbackNote brlRate={brlRate} className="mt-2" />
112125
</div>
113126

114127
<div>

src/components/treasury/TokenHoldings.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { cache } from "react";
22
import { headers } from "next/headers";
33
import { TREASURY_TOKEN_ADDRESSES } from "@/lib/config";
4+
import { getBrlRateForRequest } from "@/services/exchange-rate";
45
import { getTokenPricesUsd } from "@/services/prices";
56
import { EnrichedToken, TokenHoldingsClient } from "./TokenHoldingsClient";
67

@@ -156,5 +157,6 @@ export async function TokenHoldings({ treasuryAddress }: TokenHoldingsProps) {
156157
} catch (err) {
157158
error = err instanceof Error ? err.message : "Failed to load token holdings";
158159
}
159-
return <TokenHoldingsClient tokens={tokens} error={error} />;
160+
const brlRate = await getBrlRateForRequest();
161+
return <TokenHoldingsClient tokens={tokens} error={error} brlRate={brlRate} />;
160162
}

src/components/treasury/TokenHoldingsClient.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import { useLocale, useTranslations } from "next-intl";
44
import Image from "next/image";
55
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6+
import { formatFiatUsd } from "@/lib/i18n/fiat";
67
import { toIntlLocale } from "@/lib/i18n/format";
8+
import { FiatFallbackNote } from "./FiatFallbackNote";
79

810
export interface EnrichedToken {
911
contractAddress: string;
@@ -19,9 +21,11 @@ export interface EnrichedToken {
1921
interface TokenHoldingsClientProps {
2022
tokens: EnrichedToken[];
2123
error?: string | null;
24+
/** USD→BRL rate for value display on pt-br; `null` keeps USD. */
25+
brlRate?: number | null;
2226
}
2327

24-
export function TokenHoldingsClient({ tokens, error }: TokenHoldingsClientProps) {
28+
export function TokenHoldingsClient({ tokens, error, brlRate = null }: TokenHoldingsClientProps) {
2529
const t = useTranslations("treasury");
2630
const locale = useLocale();
2731
const intlLocale = toIntlLocale(locale);
@@ -36,12 +40,7 @@ export function TokenHoldingsClient({ tokens, error }: TokenHoldingsClientProps)
3640
const formatUsdValue = (value: number | null) => {
3741
// A real balance we couldn't price reads as a dash, never as $0.00.
3842
if (value == null) return "—";
39-
return new Intl.NumberFormat(intlLocale, {
40-
style: "currency",
41-
currency: "USD",
42-
minimumFractionDigits: 2,
43-
maximumFractionDigits: 2,
44-
}).format(value);
43+
return formatFiatUsd(value, locale, brlRate);
4544
};
4645

4746
if (error) {
@@ -72,6 +71,7 @@ export function TokenHoldingsClient({ tokens, error }: TokenHoldingsClientProps)
7271
<CardHeader>
7372
<CardTitle>{t("tokens.title")}</CardTitle>
7473
<CardDescription>{t("tokens.description")}</CardDescription>
74+
<FiatFallbackNote brlRate={brlRate} />
7575
</CardHeader>
7676
<CardContent>
7777
{/* Two-line rows instead of a three-column table: this card lives in

src/components/treasury/TreasuryAllocation.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { getTranslations } from "next-intl/server";
1+
import { getLocale, getTranslations } from "next-intl/server";
22
import { Card, CardContent } from "@/components/ui/card";
33
import { DAO_ADDRESSES } from "@/lib/config";
4+
import { formatFiatUsd } from "@/lib/i18n/fiat";
5+
import { getBrlRateForRequest } from "@/services/exchange-rate";
46
import { loadTreasurySnapshot } from "@/services/treasury";
7+
import { FiatFallbackNote } from "./FiatFallbackNote";
58
import { loadTokenHoldings } from "./TokenHoldings";
69

710
/**
@@ -35,6 +38,8 @@ type Slice = { label: string; usd: number; color: string };
3538

3639
export async function TreasuryAllocation() {
3740
const t = await getTranslations("treasury.allocation");
41+
const locale = await getLocale();
42+
const brlRate = await getBrlRateForRequest();
3843

3944
let slices: Slice[] = [];
4045
try {
@@ -74,8 +79,7 @@ export async function TreasuryAllocation() {
7479
if (slices.length === 0) return null;
7580

7681
const total = slices.reduce((s, x) => s + x.usd, 0);
77-
const usd = (n: number) =>
78-
`$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
82+
const usd = (n: number) => formatFiatUsd(n, locale, brlRate);
7983
const pct = (n: number) => (n / total) * 100;
8084

8185
return (
@@ -113,6 +117,8 @@ export async function TreasuryAllocation() {
113117
</li>
114118
))}
115119
</ul>
120+
121+
<FiatFallbackNote brlRate={brlRate} className="mt-3" />
116122
</CardContent>
117123
</Card>
118124
);

src/components/treasury/TreasuryBalance.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getBrlRateForRequest } from "@/services/exchange-rate";
12
import { loadTreasurySnapshot } from "@/services/treasury";
23
import { TreasuryBalanceClient } from "./TreasuryBalanceClient";
34

@@ -23,5 +24,7 @@ export async function TreasuryBalance({ treasuryAddress, metric = "total" }: Tre
2324
} catch (err) {
2425
error = err instanceof Error ? err.message : "Failed to load treasury data";
2526
}
26-
return <TreasuryBalanceClient metric={metric} value={value} error={error} />;
27+
// Only the "total" metric is a fiat figure; ETH metrics never convert.
28+
const brlRate = metric === "total" ? await getBrlRateForRequest() : null;
29+
return <TreasuryBalanceClient metric={metric} value={value} error={error} brlRate={brlRate} />;
2730
}
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
"use client";
22

3+
import { useLocale } from "next-intl";
34
import { CountUp } from "@/components/ui/count-up";
45
import { Skeleton } from "@/components/ui/skeleton";
6+
import { localizeFiat } from "@/lib/i18n/fiat";
7+
import { FiatFallbackNote } from "./FiatFallbackNote";
58

69
export interface TreasuryBalanceClientProps {
710
metric: "total" | "eth" | "auctions";
811
value?: number;
912
error?: string | null;
13+
/** USD→BRL rate for the fiat metric on pt-br; `null` keeps USD. */
14+
brlRate?: number | null;
1015
}
1116

12-
export function TreasuryBalanceClient({ metric, value, error }: TreasuryBalanceClientProps) {
13-
const isUsd = metric === "total";
14-
const decimals = isUsd ? 2 : 4;
15-
const prefix = isUsd ? "$" : "";
16-
const suffix = isUsd ? "" : " ETH";
17+
export function TreasuryBalanceClient({
18+
metric,
19+
value,
20+
error,
21+
brlRate = null,
22+
}: TreasuryBalanceClientProps) {
23+
const locale = useLocale();
24+
const isFiat = metric === "total";
25+
const decimals = isFiat ? 2 : 4;
1726

1827
if (error) {
1928
return <div className="text-2xl font-semibold text-destructive">Error</div>;
@@ -23,11 +32,21 @@ export function TreasuryBalanceClient({ metric, value, error }: TreasuryBalanceC
2332
return <Skeleton className="h-8 w-32" />;
2433
}
2534

35+
const { value: displayValue, currency } = isFiat
36+
? localizeFiat(value, locale, brlRate)
37+
: { value, currency: null };
38+
const prefix =
39+
currency === "BRL" ? "R$ " : currency === "USD" ? (locale === "pt-br" ? "US$ " : "$") : "";
40+
const suffix = isFiat ? "" : " ETH";
41+
2642
return (
27-
<div className="text-2xl font-semibold text-foreground">
28-
{prefix}
29-
<CountUp value={value} decimals={decimals} className="tabular-nums" />
30-
{suffix}
43+
<div>
44+
<div className="text-2xl font-semibold text-foreground">
45+
{prefix}
46+
<CountUp value={displayValue} decimals={decimals} className="tabular-nums" />
47+
{suffix}
48+
</div>
49+
{isFiat && <FiatFallbackNote brlRate={brlRate} className="mt-1" />}
3150
</div>
3251
);
3352
}

0 commit comments

Comments
 (0)