Skip to content

Commit

Permalink
Website: Properly format currency on finances transparency page (#1049)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue authored Feb 15, 2025
1 parent cb5ea54 commit c15f5a9
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 57 deletions.
4 changes: 2 additions & 2 deletions shared/src/utils/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const FALLBACK_LANGUAGE = 'en';
interface TranslateProps {
namespace?: string;
language?: string;
context?: object;
context?: Record<string, unknown> & Partial<Intl.ResolvedNumberFormatOptions>;
}

interface TranslatorProps {
Expand Down Expand Up @@ -55,7 +55,7 @@ export class Translator {
return translator;
}

public t: TranslateFunction = <T = string>(key: string, translateProps?: TranslateProps): T => {
public t: TranslateFunction = <T = string>(key: string, translateProps?: TranslateProps) => {
return this.instance.t(key, {
ns: translateProps?.namespace || this.namespaces,
lng: translateProps?.language || this.language,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DefaultPageProps, DefaultParams } from '@/app/[lang]/[region]';
import { CurrencyRedirect } from '@/app/[lang]/[region]/(website)/transparency/(components)/currency-redirect';
import { firestoreAdmin } from '@/firebase-admin';
import { websiteCurrencies, WebsiteCurrency } from '@/i18n';
import { toLocale, websiteCurrencies, WebsiteCurrency } from '@/i18n';
import { Currency } from '@socialincome/shared/src/types/currency';
import {
ContributionStats,
Expand All @@ -23,13 +23,13 @@ export const revalidate = 3600; // update once an hour
export const generateStaticParams = () => websiteCurrencies.map((currency) => ({ currency: currency.toLowerCase() }));

export type TransparencyPageProps = DefaultPageProps & {
params: DefaultParams & { currency: string };
params: DefaultParams & { currency: WebsiteCurrency };
};

export type SectionProps = {
contributionStats: ContributionStats;
expensesStats: ExpenseStats;
params: DefaultParams & { currency: string };
params: DefaultParams & { currency: WebsiteCurrency };
paymentStats: PaymentStats;
recipientStats: RecipientStats;
};
Expand All @@ -48,7 +48,13 @@ export default async function Page({ params }: TransparencyPageProps) {
};
const currency = params.currency.toUpperCase() as WebsiteCurrency;
const { contributionStats, expensesStats, paymentStats, recipientStats } = await getStats(currency);
const sectionProps = { contributionStats, expensesStats, params, paymentStats, recipientStats };
const currencyLocales = {
style: 'currency' as keyof Intl.NumberFormatOptionsStyleRegistry,
currency: params.currency,
locale: toLocale(params.lang, params.region),
maximumFractionDigits: 0,
};
const sectionProps = { contributionStats, expensesStats, params, paymentStats, recipientStats, currencyLocales };

return (
<BaseContainer className="flex flex-col space-y-16">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toCurrencyLocale } from '@/i18n';
import { RecipientProgramStatus } from '@socialincome/shared/src/types/recipient';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { Badge, Card, CardContent, Typography } from '@socialincome/ui';
Expand All @@ -11,6 +12,7 @@ export const roundAmount = (amount: number) => {

export async function Section1({ params, paymentStats, contributionStats, recipientStats }: SectionProps) {
const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-finances'] });
const currencyLocale = toCurrencyLocale(params.lang, params.region, params.currency, { maximumFractionDigits: 0 });

return (
<div>
Expand Down Expand Up @@ -60,8 +62,7 @@ export async function Section1({ params, paymentStats, contributionStats, recipi
context: {
contributorCount: contributionStats.totalContributorsCount,
value: roundAmount(contributionStats.totalContributionsAmount),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { roundAmount } from '@/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-1';
import { toCurrencyLocale } from '@/i18n';
import { HeartIcon } from '@heroicons/react/24/outline';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { Badge, Typography } from '@socialincome/ui';
Expand All @@ -9,6 +10,7 @@ import { SectionProps } from './page';
export async function Section2({ params, contributionStats, expensesStats, paymentStats }: SectionProps) {
const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-finances'] });
const expensesProject = _.sum(Object.values(expensesStats.totalExpensesByType));
const currencyLocale = toCurrencyLocale(params.lang, params.region, params.currency, { maximumFractionDigits: 0 });

return (
<div>
Expand All @@ -18,11 +20,7 @@ export async function Section2({ params, contributionStats, expensesStats, payme
<InfoCard
sectionTitle={translator.t('section-2.donations')}
title={translator.t('amount', {
context: {
value: roundAmount(contributionStats.totalContributionsAmount),
currency: params.currency,
maximumFractionDigits: 0,
},
context: { value: roundAmount(contributionStats.totalContributionsAmount), ...currencyLocale },
})}
text={translator.t('section-2.amount-since-march-2020')}
firstIcon={<HeartIcon className="h-8 w-8" />}
Expand All @@ -33,8 +31,7 @@ export async function Section2({ params, contributionStats, expensesStats, payme
{translator.t('section-2.contributions-from', {
context: {
value: roundAmount(contributionStats.totalIndividualContributionsAmount),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -48,8 +45,7 @@ export async function Section2({ params, contributionStats, expensesStats, payme
{translator.t('section-2.past-payouts', {
context: {
value: roundAmount(paymentStats.totalPaymentsAmount),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -59,8 +55,7 @@ export async function Section2({ params, contributionStats, expensesStats, payme
value: roundAmount(
contributionStats.totalIndividualContributionsAmount - paymentStats.totalPaymentsAmount,
),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -74,8 +69,7 @@ export async function Section2({ params, contributionStats, expensesStats, payme
{translator.t('section-2.contributions-from', {
context: {
value: roundAmount(contributionStats.totalInstitutionalContributionsAmount),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -89,17 +83,15 @@ export async function Section2({ params, contributionStats, expensesStats, payme
{translator.t('section-2.past-costs', {
context: {
value: roundAmount(expensesProject),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
<Typography>
{translator.t('section-2.future-costs', {
context: {
value: roundAmount(contributionStats.totalInstitutionalContributionsAmount - expensesProject),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { roundAmount } from '@/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-1';
import { toCurrencyLocale } from '@/i18n';
import { CountryCode } from '@socialincome/shared/src/types/country';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { Typography } from '@socialincome/ui';
Expand All @@ -15,6 +16,7 @@ export async function Section3({ params, contributionStats }: SectionProps) {
amount: number;
usersCount: number;
}[];
const currencyLocale = toCurrencyLocale(params.lang, params.region, params.currency, { maximumFractionDigits: 0 });

return (
<div>
Expand All @@ -37,8 +39,7 @@ export async function Section3({ params, contributionStats }: SectionProps) {
context: {
contributorsCount: entry.usersCount,
value: roundAmount(entry.amount),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
}),
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { firestoreAdmin } from '@/firebase-admin';
import { toCurrencyLocale } from '@/i18n';
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import { BanknotesIcon, BuildingLibraryIcon, DevicePhoneMobileIcon } from '@heroicons/react/24/solid';
import { PAYMENT_AMOUNT_SLE } from '@socialincome/shared/src/types/payment';
Expand All @@ -15,6 +16,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
const expensesTotal = _.sum(Object.values(expensesStats.totalExpensesByType)) + paymentStats.totalPaymentsAmount;
const reservesTotal = contributionStats.totalContributionsAmount - expensesTotal;
const exchangeRateSLE = await getLatestExchangeRate(firestoreAdmin, 'SLE');
const currencyLocale = toCurrencyLocale(params.lang, params.region, params.currency, { maximumFractionDigits: 0 });

return (
<div className="flex flex-col space-y-8">
Expand All @@ -28,8 +30,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
title={translator.t('amount', {
context: {
value: roundAmount(expensesTotal),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
text={translator.t('section-4.amount-since-march-2020')}
Expand All @@ -41,9 +42,8 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.payments-total', {
context: {
value: roundAmount(paymentStats.totalPaymentsAmount),
currency: params.currency,
maximumFractionDigits: 0,
recipientsCount: paymentStats.totalRecipientsCount,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -52,9 +52,8 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.payments-last-month', {
context: {
value: roundAmount(_.last(paymentStats.totalPaymentsByMonth)?.amount as number),
currency: params.currency,
maximumFractionDigits: 0,
recipientsCount: _.last(paymentStats.totalPaymentsByMonth)?.recipientsCount,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -64,8 +63,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
value: roundAmount(
contributionStats.totalIndividualContributionsAmount - paymentStats.totalPaymentsAmount,
),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -79,8 +77,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.project-costs', {
context: {
value: roundAmount(_.sum(Object.values(expensesStats.totalExpensesByType))),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
</Typography>
Expand All @@ -89,8 +86,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.donation-fees', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.donation_fees),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -106,8 +102,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.delivery-fees', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.delivery_fees),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -123,8 +118,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.exchange-rate-loss', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.exchange_rate_loss),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -140,8 +134,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.account-fees', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.account_fees),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -158,8 +151,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.staff-costs', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.staff),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -175,8 +167,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.fundraising-costs', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.fundraising_advertising),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -192,8 +183,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
{translator.t('section-4.administrative-costs', {
context: {
value: roundAmount(expensesStats.totalExpensesByType.administrative),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
<TooltipProvider delayDuration={100}>
Expand All @@ -214,8 +204,7 @@ export async function Section4({ params, expensesStats, paymentStats, contributi
title={translator.t('amount', {
context: {
value: roundAmount(reservesTotal),
currency: params.currency,
maximumFractionDigits: 0,
...currencyLocale,
},
})}
text={translator.t('section-4.amount-since-march-2020')}
Expand Down
26 changes: 22 additions & 4 deletions website/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,34 @@ export type WebsiteCurrency = Extract<Currency, 'USD' | 'EUR' | 'CHF' | 'SLE'>;
export const defaultCurrency: WebsiteCurrency = 'USD';
export const websiteCurrencies: WebsiteCurrency[] = ['USD', 'EUR', 'CHF'];

export const toLocale = (language: WebsiteLanguage, region: WebsiteRegion): string => {
return language + (region === 'ch' ? '-CH' : '');
};

export const toCurrencyLocale = (
language: WebsiteLanguage,
region: WebsiteRegion,
currency: WebsiteCurrency,
options?: Partial<Intl.ResolvedNumberFormatOptions>,
): Partial<Intl.ResolvedNumberFormatOptions> => {
return {
style: 'currency' as keyof Intl.NumberFormatOptionsStyleRegistry,
currency: currency,
locale: toLocale(language, region),
...options,
};
};

/**
* Check if the user has set a language and region cookie, and if they are valid. If so, return them.
* Otherwise, try to find the best locale from the Accept-Language header, and if that fails, return the default locale.
*/
export const findBestLocale = (
request: NextRequest,
): {
region: WebsiteRegion;
language: WebsiteLanguage;
} => {
/**
* Check if the user has set a language and region cookie, and if they are valid. If so, return them.
* Otherwise, try to find the best locale from the Accept-Language header, and if that fails, return the default locale.
*/
if (
request.cookies.has(LANGUAGE_COOKIE) &&
mainWebsiteLanguages.includes(request.cookies.get(LANGUAGE_COOKIE)!.value as WebsiteLanguage) &&
Expand Down

0 comments on commit c15f5a9

Please sign in to comment.