diff --git a/.changeset/fair-currency-hugs.md b/.changeset/fair-currency-hugs.md
new file mode 100644
index 00000000000..1096406894e
--- /dev/null
+++ b/.changeset/fair-currency-hugs.md
@@ -0,0 +1,6 @@
+---
+'@clerk/localizations': minor
+'@clerk/ui': minor
+---
+
+Monetary amounts are now formatted using your application's locale. For example, with the locale set to `fr-FR`, a USD 1000 amount now renders as `1 000,00 $US`; previously, it rendered as `$1,000.00` regardless of your application's configured locale.
\ No newline at end of file
diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts
index 5c31adb3a63..2d1265b96d3 100644
--- a/packages/localizations/src/bn-IN.ts
+++ b/packages/localizations/src/bn-IN.ts
@@ -209,9 +209,9 @@ export const bnIN: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'এই প্ল্যানে পরিবর্তন করুন',
switchToAnnual: 'বার্ষিকে স্যুইচ করুন',
- switchToAnnualWithAnnualPrice: 'বার্ষিকে স্যুইচ করুন {{currency}}{{price}} / বছর',
+ switchToAnnualWithAnnualPrice: 'বার্ষিকে স্যুইচ করুন {{price}} / বছর',
switchToMonthly: 'মাসিকে স্যুইচ করুন',
- switchToMonthlyWithPrice: 'মাসিকে স্যুইচ করুন {{currency}}{{price}} / মাস',
+ switchToMonthlyWithPrice: 'মাসিকে স্যুইচ করুন {{price}} / মাস',
totalDue: 'মোট বকেয়া',
totalDuePerPeriod: undefined,
totalDueToday: 'আজকের মোট বকেয়া',
diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts
index 379be19a675..3ee8d4cb8e1 100644
--- a/packages/localizations/src/ca-ES.ts
+++ b/packages/localizations/src/ca-ES.ts
@@ -210,9 +210,9 @@ export const caES: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Canviar a aquest pla',
switchToAnnual: 'Canviar a anual',
- switchToAnnualWithAnnualPrice: 'Canviar a anual {{currency}}{{price}} / any',
+ switchToAnnualWithAnnualPrice: 'Canviar a anual {{price}} / any',
switchToMonthly: 'Canviar a mensual',
- switchToMonthlyWithPrice: 'Canviar a mensual {{currency}}{{price}} / mes',
+ switchToMonthlyWithPrice: 'Canviar a mensual {{price}} / mes',
totalDue: 'Total a pagar',
totalDuePerPeriod: undefined,
totalDueToday: 'Total a pagar avui',
diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts
index 546aefec99c..1cec03eed31 100644
--- a/packages/localizations/src/cs-CZ.ts
+++ b/packages/localizations/src/cs-CZ.ts
@@ -207,9 +207,9 @@ export const csCZ: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Přepnout na tento plán',
switchToAnnual: 'Přepnout na roční',
- switchToAnnualWithAnnualPrice: 'Přepnout na roční {{currency}}{{price}} / rok',
+ switchToAnnualWithAnnualPrice: 'Přepnout na roční {{price}} / rok',
switchToMonthly: 'Přepnout na měsíční',
- switchToMonthlyWithPrice: 'Přepnout na měsíční {{currency}}{{price}} / měsíc',
+ switchToMonthlyWithPrice: 'Přepnout na měsíční {{price}} / měsíc',
totalDue: 'Celkem k zaplacení',
totalDuePerPeriod: undefined,
totalDueToday: 'Celkem k zaplacení dnes',
diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts
index 04cc0e3c58a..fa8032421b6 100644
--- a/packages/localizations/src/de-DE.ts
+++ b/packages/localizations/src/de-DE.ts
@@ -209,9 +209,9 @@ export const deDE: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Zu diesem Plan wechseln',
switchToAnnual: 'Wechsel zu jährlich',
- switchToAnnualWithAnnualPrice: 'Auf jährlich wechseln {{currency}}{{price}} / Jahr',
+ switchToAnnualWithAnnualPrice: 'Auf jährlich wechseln {{price}} / Jahr',
switchToMonthly: 'Wechsel zu monatlich',
- switchToMonthlyWithPrice: 'Auf monatlich wechseln {{currency}}{{price}} / Monat',
+ switchToMonthlyWithPrice: 'Auf monatlich wechseln {{price}} / Monat',
totalDue: 'Gesamtbetrag',
totalDuePerPeriod: undefined,
totalDueToday: 'Heute fällig',
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index 2bcd237dcff..b46f8bcd5cf 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -198,9 +198,9 @@ export const enUS: LocalizationResource = {
subtotalRenewal: 'Subtotal per period',
switchPlan: 'Switch to this plan',
switchToAnnual: 'Switch to annual',
- switchToAnnualWithAnnualPrice: 'Switch to annual {{currency}}{{price}} / year',
+ switchToAnnualWithAnnualPrice: 'Switch to annual {{price}} / year',
switchToMonthly: 'Switch to monthly',
- switchToMonthlyWithPrice: 'Switch to monthly {{currency}}{{price}} / month',
+ switchToMonthlyWithPrice: 'Switch to monthly {{price}} / month',
totalDue: 'Total due',
totalDuePerPeriod: 'Total per period',
totalDueToday: 'Total due today',
diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts
index 46ad8e7d6ad..1c47f9de641 100644
--- a/packages/localizations/src/es-ES.ts
+++ b/packages/localizations/src/es-ES.ts
@@ -209,9 +209,9 @@ export const esES: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Cambiar a este plan',
switchToAnnual: 'Cambiar a anual',
- switchToAnnualWithAnnualPrice: 'Cambiar a anual {{currency}}{{price}} / año',
+ switchToAnnualWithAnnualPrice: 'Cambiar a anual {{price}} / año',
switchToMonthly: 'Cambiar a mensual',
- switchToMonthlyWithPrice: 'Cambiar a mensual {{currency}}{{price}} / mes',
+ switchToMonthlyWithPrice: 'Cambiar a mensual {{price}} / mes',
totalDue: 'Total a pagar',
totalDuePerPeriod: undefined,
totalDueToday: 'Total a pagar hoy',
diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts
index b8fd2b146c4..9617061dd2f 100644
--- a/packages/localizations/src/fa-IR.ts
+++ b/packages/localizations/src/fa-IR.ts
@@ -208,9 +208,9 @@ export const faIR: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'به این طرح تغییر دهید',
switchToAnnual: 'به سالانه تغییر دهید',
- switchToAnnualWithAnnualPrice: 'تغییر به سالانه {{currency}}{{price}} / سال',
+ switchToAnnualWithAnnualPrice: 'تغییر به سالانه {{price}} / سال',
switchToMonthly: 'به ماهانه تغییر دهید',
- switchToMonthlyWithPrice: 'تغییر به ماهانه {{currency}}{{price}} / ماه',
+ switchToMonthlyWithPrice: 'تغییر به ماهانه {{price}} / ماه',
totalDue: 'کل مبلغ سررسید',
totalDuePerPeriod: undefined,
totalDueToday: 'سررسید کل امروز',
diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts
index 91898b28323..bc37fca6fb3 100644
--- a/packages/localizations/src/fr-FR.ts
+++ b/packages/localizations/src/fr-FR.ts
@@ -211,9 +211,9 @@ export const frFR: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Changer de plan',
switchToAnnual: "Passer à l'annuel",
- switchToAnnualWithAnnualPrice: 'Passer à l’annuel {{currency}}{{price}} / an',
+ switchToAnnualWithAnnualPrice: 'Passer à l’annuel {{price}} / an',
switchToMonthly: 'Passer au mensuel',
- switchToMonthlyWithPrice: 'Passer au mensuel {{currency}}{{price}} / mois',
+ switchToMonthlyWithPrice: 'Passer au mensuel {{price}} / mois',
totalDue: 'Total dû',
totalDuePerPeriod: undefined,
totalDueToday: "Total dû aujourd'hui",
diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts
index 2a84526d180..024ba122277 100644
--- a/packages/localizations/src/hi-IN.ts
+++ b/packages/localizations/src/hi-IN.ts
@@ -209,9 +209,9 @@ export const hiIN: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'इस योजना पर स्विच करें',
switchToAnnual: 'वार्षिक पर स्विच करें',
- switchToAnnualWithAnnualPrice: 'वार्षिक पर स्विच करें {{currency}}{{price}} / वर्ष',
+ switchToAnnualWithAnnualPrice: 'वार्षिक पर स्विच करें {{price}} / वर्ष',
switchToMonthly: 'मासिक पर स्विच करें',
- switchToMonthlyWithPrice: 'मासिक पर स्विच करें {{currency}}{{price}} / माह',
+ switchToMonthlyWithPrice: 'मासिक पर स्विच करें {{price}} / माह',
totalDue: 'कुल देय',
totalDuePerPeriod: undefined,
totalDueToday: 'आज का कुल देय',
diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts
index ec4336ac30f..75d5b107ba8 100644
--- a/packages/localizations/src/hr-HR.ts
+++ b/packages/localizations/src/hr-HR.ts
@@ -210,9 +210,9 @@ export const hrHR: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Prebaci na ovaj plan',
switchToAnnual: 'Prebaci na godišnje',
- switchToAnnualWithAnnualPrice: 'Prebaci na godišnje {{currency}}{{price}} / godišnje',
+ switchToAnnualWithAnnualPrice: 'Prebaci na godišnje {{price}} / godišnje',
switchToMonthly: 'Prebaci na mjesečno',
- switchToMonthlyWithPrice: 'Prebaci na mjesečno {{currency}}{{price}} / mjesečno',
+ switchToMonthlyWithPrice: 'Prebaci na mjesečno {{price}} / mjesečno',
totalDue: 'Ukupno za platiti',
totalDuePerPeriod: undefined,
totalDueToday: 'Ukupno za platiti danas',
diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts
index b2267b1e0b8..dd37bbea450 100644
--- a/packages/localizations/src/hu-HU.ts
+++ b/packages/localizations/src/hu-HU.ts
@@ -210,9 +210,9 @@ export const huHU: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Váltás erre a csomagra',
switchToAnnual: 'Váltás éves fizetésre',
- switchToAnnualWithAnnualPrice: 'Váltás éves fizetésre: {{currency}}{{price}} / év',
+ switchToAnnualWithAnnualPrice: 'Váltás éves fizetésre: {{price}} / év',
switchToMonthly: 'Váltás havi fizetésre',
- switchToMonthlyWithPrice: 'Váltás havi fizetésre: {{currency}}{{price}} / hó',
+ switchToMonthlyWithPrice: 'Váltás havi fizetésre: {{price}} / hó',
totalDue: 'Fizetendő összeg',
totalDuePerPeriod: undefined,
totalDueToday: 'Mai fizetendő összeg',
diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts
index e4336d1e540..304b38ea9e7 100644
--- a/packages/localizations/src/is-IS.ts
+++ b/packages/localizations/src/is-IS.ts
@@ -209,9 +209,9 @@ export const isIS: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Skipta yfir í þessa áskrift',
switchToAnnual: 'Skipta yfir í árlega',
- switchToAnnualWithAnnualPrice: 'Skipta yfir í árlega {{currency}}{{price}} / ár',
+ switchToAnnualWithAnnualPrice: 'Skipta yfir í árlega {{price}} / ár',
switchToMonthly: 'Skipta yfir í mánaðarlega',
- switchToMonthlyWithPrice: 'Skipta yfir í mánaðarlega {{currency}}{{price}} / mánuð',
+ switchToMonthlyWithPrice: 'Skipta yfir í mánaðarlega {{price}} / mánuð',
totalDue: 'Samtals til greiðslu',
totalDuePerPeriod: undefined,
totalDueToday: 'Samtals til greiðslu í dag',
diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts
index ff37d798a7a..f86e1beb8c7 100644
--- a/packages/localizations/src/it-IT.ts
+++ b/packages/localizations/src/it-IT.ts
@@ -209,9 +209,9 @@ export const itIT: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Passa a questo piano',
switchToAnnual: 'Passa ad annuale',
- switchToAnnualWithAnnualPrice: 'Passa ad annuale {{currency}}{{price}} / anno',
+ switchToAnnualWithAnnualPrice: 'Passa ad annuale {{price}} / anno',
switchToMonthly: 'Passa a mensile',
- switchToMonthlyWithPrice: 'Passa a mensile {{currency}}{{price}} / mese',
+ switchToMonthlyWithPrice: 'Passa a mensile {{price}} / mese',
totalDue: 'Totale dovuto',
totalDuePerPeriod: undefined,
totalDueToday: 'Totale dovuto oggi',
diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts
index 68f76cb6852..de7e8af80fd 100644
--- a/packages/localizations/src/ja-JP.ts
+++ b/packages/localizations/src/ja-JP.ts
@@ -210,9 +210,9 @@ export const jaJP: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'このプランに切り替える',
switchToAnnual: '年払いに切り替える',
- switchToAnnualWithAnnualPrice: '年払い {{currency}}{{price}} / 年 に切り替える',
+ switchToAnnualWithAnnualPrice: '年払い {{price}} / 年 に切り替える',
switchToMonthly: '月払いに切り替える',
- switchToMonthlyWithPrice: '月払い {{currency}}{{price}} / 月 に切り替える',
+ switchToMonthlyWithPrice: '月払い {{price}} / 月 に切り替える',
totalDue: '支払い合計',
totalDuePerPeriod: undefined,
totalDueToday: '本日のお支払合計',
diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts
index 6b0c5185277..ac2474aa63e 100644
--- a/packages/localizations/src/ko-KR.ts
+++ b/packages/localizations/src/ko-KR.ts
@@ -207,9 +207,9 @@ export const koKR: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: '이 플랜으로 전환',
switchToAnnual: '연간 결제로 변경',
- switchToAnnualWithAnnualPrice: '연간 {{currency}}{{price}} / 년으로 변경',
+ switchToAnnualWithAnnualPrice: '연간 {{price}} / 년으로 변경',
switchToMonthly: '월간 결제로 변경',
- switchToMonthlyWithPrice: '월간 {{currency}}{{price}} / 월로 변경',
+ switchToMonthlyWithPrice: '월간 {{price}} / 월로 변경',
totalDue: '총 결제 금액',
totalDuePerPeriod: undefined,
totalDueToday: '오늘 결제 금액',
diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts
index ba1f0c849c9..1effc60b7a0 100644
--- a/packages/localizations/src/ms-MY.ts
+++ b/packages/localizations/src/ms-MY.ts
@@ -211,9 +211,9 @@ export const msMY: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Tukar ke pelan ini',
switchToAnnual: 'Tukar kepada tahunan',
- switchToAnnualWithAnnualPrice: 'Tukar kepada tahunan {{currency}}{{price}} / tahun',
+ switchToAnnualWithAnnualPrice: 'Tukar kepada tahunan {{price}} / tahun',
switchToMonthly: 'Tukar kepada bulanan',
- switchToMonthlyWithPrice: 'Tukar kepada bulanan {{currency}}{{price}} / bulan',
+ switchToMonthlyWithPrice: 'Tukar kepada bulanan {{price}} / bulan',
totalDue: 'Jumlah perlu dibayar',
totalDuePerPeriod: undefined,
totalDueToday: 'Jumlah Perlu Dibayar Hari Ini',
diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts
index bf548934b95..60d59c5652c 100644
--- a/packages/localizations/src/nb-NO.ts
+++ b/packages/localizations/src/nb-NO.ts
@@ -210,9 +210,9 @@ export const nbNO: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Bytt til denne planen',
switchToAnnual: 'Bytt til årlig',
- switchToAnnualWithAnnualPrice: 'Bytt til årlig {{currency}}{{price}} / år',
+ switchToAnnualWithAnnualPrice: 'Bytt til årlig {{price}} / år',
switchToMonthly: 'Bytt til månedlig',
- switchToMonthlyWithPrice: 'Bytt til månedlig {{currency}}{{price}} / måned',
+ switchToMonthlyWithPrice: 'Bytt til månedlig {{price}} / måned',
totalDue: 'Totalt å betale',
totalDuePerPeriod: undefined,
totalDueToday: 'Totalt å betale i dag',
diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts
index 1fb681a6335..6e353384335 100644
--- a/packages/localizations/src/pt-BR.ts
+++ b/packages/localizations/src/pt-BR.ts
@@ -209,9 +209,9 @@ export const ptBR: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Mudar de plano',
switchToAnnual: 'Mudar para anual',
- switchToAnnualWithAnnualPrice: 'Mudar para anual {{currency}}{{price}} / ano',
+ switchToAnnualWithAnnualPrice: 'Mudar para anual {{price}} / ano',
switchToMonthly: 'Mudar para mensal',
- switchToMonthlyWithPrice: 'Mudar para mensal {{currency}}{{price}} / mês',
+ switchToMonthlyWithPrice: 'Mudar para mensal {{price}} / mês',
totalDue: 'Total devido',
totalDuePerPeriod: undefined,
totalDueToday: 'Total devido hoje',
diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts
index 02ddddab011..053abb0ef40 100644
--- a/packages/localizations/src/pt-PT.ts
+++ b/packages/localizations/src/pt-PT.ts
@@ -211,9 +211,9 @@ export const ptPT: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Mudar para este plano',
switchToAnnual: 'Mudar para anual',
- switchToAnnualWithAnnualPrice: 'Mudar para anual {{currency}}{{price}} / ano',
+ switchToAnnualWithAnnualPrice: 'Mudar para anual {{price}} / ano',
switchToMonthly: 'Mudar para mensal',
- switchToMonthlyWithPrice: 'Mudar para mensal {{currency}}{{price}} / mês',
+ switchToMonthlyWithPrice: 'Mudar para mensal {{price}} / mês',
totalDue: 'Total devido',
totalDuePerPeriod: undefined,
totalDueToday: 'Total devido hoje',
diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts
index 141f902c02c..069f1176d83 100644
--- a/packages/localizations/src/ro-RO.ts
+++ b/packages/localizations/src/ro-RO.ts
@@ -209,9 +209,9 @@ export const roRO: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Schimbă pe acest plan',
switchToAnnual: 'Treci la anual',
- switchToAnnualWithAnnualPrice: 'Treci la anual {{currency}}{{price}} / an',
+ switchToAnnualWithAnnualPrice: 'Treci la anual {{price}} / an',
switchToMonthly: 'Treci la lunar',
- switchToMonthlyWithPrice: 'Treci la lunar {{currency}}{{price}} / lună',
+ switchToMonthlyWithPrice: 'Treci la lunar {{price}} / lună',
totalDue: 'Total de plată',
totalDuePerPeriod: undefined,
totalDueToday: 'Total de plată astăzi',
diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts
index 47912167626..c9b2a61def1 100644
--- a/packages/localizations/src/ta-IN.ts
+++ b/packages/localizations/src/ta-IN.ts
@@ -211,9 +211,9 @@ export const taIN: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'இந்த திட்டத்திற்கு மாறவும்',
switchToAnnual: 'வருடாந்திரத்திற்கு மாறு',
- switchToAnnualWithAnnualPrice: 'வருடாந்திரத்திற்கு மாறு {{currency}}{{price}} / ஆண்டு',
+ switchToAnnualWithAnnualPrice: 'வருடாந்திரத்திற்கு மாறு {{price}} / ஆண்டு',
switchToMonthly: 'மாதாந்திரத்திற்கு மாறு',
- switchToMonthlyWithPrice: 'மாதாந்திரத்திற்கு மாறு {{currency}}{{price}} / மாதம்',
+ switchToMonthlyWithPrice: 'மாதாந்திரத்திற்கு மாறு {{price}} / மாதம்',
totalDue: 'செலுத்த வேண்டிய மொத்தம்',
totalDuePerPeriod: undefined,
totalDueToday: 'இன்று செலுத்த வேண்டிய மொத்தம்',
diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts
index 00fe21b6d9e..47face6d07a 100644
--- a/packages/localizations/src/te-IN.ts
+++ b/packages/localizations/src/te-IN.ts
@@ -210,9 +210,9 @@ export const teIN: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'ఈ ప్లాన్కు మారండి',
switchToAnnual: 'వార్షికానికి మార్చు',
- switchToAnnualWithAnnualPrice: 'వార్షికానికి మార్చు {{currency}}{{price}} / సంవత్సరం',
+ switchToAnnualWithAnnualPrice: 'వార్షికానికి మార్చు {{price}} / సంవత్సరం',
switchToMonthly: 'నెలవారీకి మార్చు',
- switchToMonthlyWithPrice: 'నెలవారీకి మార్చు {{currency}}{{price}} / నెల',
+ switchToMonthlyWithPrice: 'నెలవారీకి మార్చు {{price}} / నెల',
totalDue: 'చెల్లించవలసిన మొత్తం',
totalDuePerPeriod: undefined,
totalDueToday: 'ఈరోజు చెల్లించవలసిన మొత్తం',
diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts
index 2808d84f693..92928ab3e8f 100644
--- a/packages/localizations/src/th-TH.ts
+++ b/packages/localizations/src/th-TH.ts
@@ -207,9 +207,9 @@ export const thTH: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'เปลี่ยนไปใช้แผนนี้',
switchToAnnual: 'เปลี่ยนเป็นรายปี',
- switchToAnnualWithAnnualPrice: 'เปลี่ยนเป็นรายปี {{currency}}{{price}} / ปี',
+ switchToAnnualWithAnnualPrice: 'เปลี่ยนเป็นรายปี {{price}} / ปี',
switchToMonthly: 'เปลี่ยนเป็นรายเดือน',
- switchToMonthlyWithPrice: 'เปลี่ยนเป็นรายเดือน {{currency}}{{price}} / เดือน',
+ switchToMonthlyWithPrice: 'เปลี่ยนเป็นรายเดือน {{price}} / เดือน',
totalDue: 'ยอดรวมที่ต้องชำระ',
totalDuePerPeriod: undefined,
totalDueToday: 'ยอดรวมที่ต้องชำระวันนี้',
diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts
index cd70866c314..26881e97b98 100644
--- a/packages/localizations/src/vi-VN.ts
+++ b/packages/localizations/src/vi-VN.ts
@@ -209,9 +209,9 @@ export const viVN: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: 'Chuyển sang gói này',
switchToAnnual: 'Chuyển sang hàng năm',
- switchToAnnualWithAnnualPrice: 'Chuyển sang gói năm {{currency}}{{price}} / năm',
+ switchToAnnualWithAnnualPrice: 'Chuyển sang gói năm {{price}} / năm',
switchToMonthly: 'Chuyển sang hàng tháng',
- switchToMonthlyWithPrice: 'Chuyển sang gói tháng {{currency}}{{price}} / tháng',
+ switchToMonthlyWithPrice: 'Chuyển sang gói tháng {{price}} / tháng',
totalDue: 'Tổng cần thanh toán',
totalDuePerPeriod: undefined,
totalDueToday: 'Tổng cần thanh toán hôm nay',
diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts
index 327efb9d13d..f5dcc0c1aea 100644
--- a/packages/localizations/src/zh-TW.ts
+++ b/packages/localizations/src/zh-TW.ts
@@ -206,9 +206,9 @@ export const zhTW: LocalizationResource = {
subtotalRenewal: undefined,
switchPlan: '切換到此方案',
switchToAnnual: '切換到每年',
- switchToAnnualWithAnnualPrice: '切換到每年 {{currency}}{{price}} / 年',
+ switchToAnnualWithAnnualPrice: '切換到每年 {{price}} / 年',
switchToMonthly: '切換到每月',
- switchToMonthlyWithPrice: '切換到每月 {{currency}}{{price}} / 月',
+ switchToMonthlyWithPrice: '切換到每月 {{price}} / 月',
totalDue: '總逾期金額',
totalDuePerPeriod: undefined,
totalDueToday: '總逾期金額',
diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts
index cef53127261..8bab00dfa15 100644
--- a/packages/shared/src/types/localization.ts
+++ b/packages/shared/src/types/localization.ts
@@ -205,8 +205,8 @@ export type __internal_LocalizationResource = {
switchPlan: LocalizationValue;
switchToMonthly: LocalizationValue;
switchToAnnual: LocalizationValue;
- switchToMonthlyWithPrice: LocalizationValue<'price' | 'currency'>;
- switchToAnnualWithAnnualPrice: LocalizationValue<'price' | 'currency'>;
+ switchToMonthlyWithPrice: LocalizationValue<'price'>;
+ switchToAnnualWithAnnualPrice: LocalizationValue<'price'>;
billedAnnually: LocalizationValue;
billedMonthly: LocalizationValue;
billedMonthlyOnly: LocalizationValue;
diff --git a/packages/ui/src/components/Checkout/CheckoutComplete.tsx b/packages/ui/src/components/Checkout/CheckoutComplete.tsx
index 21877d2bdb8..3f6c2a1932c 100644
--- a/packages/ui/src/components/Checkout/CheckoutComplete.tsx
+++ b/packages/ui/src/components/Checkout/CheckoutComplete.tsx
@@ -6,7 +6,17 @@ import { LineItems } from '@/ui/elements/LineItems';
import { formatDate } from '@/ui/utils/formatDate';
import { useCheckoutContext } from '../../contexts';
-import { Box, Button, descriptors, Heading, localizationKeys, Span, Text, useAppearance } from '../../customizables';
+import {
+ Box,
+ Button,
+ descriptors,
+ Heading,
+ localizationKeys,
+ Span,
+ Text,
+ useAppearance,
+ useLocalizations,
+} from '../../customizables';
import { transitionDurationValues, transitionTiming } from '../../foundations/transitions';
import { usePrefersReducedMotion } from '../../hooks';
import { useRouter } from '../../router';
@@ -164,6 +174,7 @@ export const CheckoutComplete = () => {
const { checkout } = useCheckout();
const { totals, paymentMethod, planPeriodStart, freeTrialEndsAt } = checkout;
const [mousePosition, setMousePosition] = useState({ x: 256, y: 256 });
+ const { $ } = useLocalizations();
const prefersReducedMotion = usePrefersReducedMotion();
const { animations: layoutAnimations } = useAppearance().parsedOptions;
@@ -420,9 +431,7 @@ export const CheckoutComplete = () => {
{totals.totalDueNow ? (
-
+
) : null}
diff --git a/packages/ui/src/components/Checkout/CheckoutForm.tsx b/packages/ui/src/components/Checkout/CheckoutForm.tsx
index 935132c9981..953c48d0d01 100644
--- a/packages/ui/src/components/Checkout/CheckoutForm.tsx
+++ b/packages/ui/src/components/Checkout/CheckoutForm.tsx
@@ -19,7 +19,18 @@ import { handleError } from '@/ui/utils/errorHandler';
import { DevOnly } from '../../common/DevOnly';
import { useCheckoutContext, usePaymentMethods } from '../../contexts';
-import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Spinner, Text } from '../../customizables';
+import {
+ Box,
+ Button,
+ Col,
+ descriptors,
+ Flex,
+ Form,
+ localizationKeys,
+ Spinner,
+ Text,
+ useLocalizations,
+} from '../../customizables';
import { ChevronUpDown, InformationCircle } from '../../icons';
import type { PropsOfComponent, ThemableCssProp } from '../../styledSystem';
import * as AddPaymentMethod from '../PaymentMethods/AddPaymentMethod';
@@ -34,6 +45,7 @@ const HIDDEN_INPUT_NAME = 'payment_method_id';
export const CheckoutForm = withCardStateProvider(() => {
const { checkout } = useCheckout();
+ const { $ } = useLocalizations();
const { plan, totals, isImmediatePlanChange, planPeriod, freeTrialEndsAt } = checkout;
@@ -102,7 +114,7 @@ export const CheckoutForm = withCardStateProvider(() => {
{totals.baseFee ? (
) : null}
@@ -112,7 +124,7 @@ export const CheckoutForm = withCardStateProvider(() => {
@@ -121,7 +133,7 @@ export const CheckoutForm = withCardStateProvider(() => {
)}
@@ -129,7 +141,7 @@ export const CheckoutForm = withCardStateProvider(() => {
)}
@@ -137,7 +149,7 @@ export const CheckoutForm = withCardStateProvider(() => {
)}
@@ -152,7 +164,7 @@ export const CheckoutForm = withCardStateProvider(() => {
-
+
)}
@@ -163,9 +175,7 @@ export const CheckoutForm = withCardStateProvider(() => {
days: plan.freeTrialDays,
})}
/>
-
+
) : showRenewalTotals ? null : totals.totalDuePerPeriod ? (
{
variant='tertiary'
>
-
+
) : null}
{totals.totalDueNow ? (
-
+
) : null}
{showRenewalTotals && (
-
+
)}
@@ -413,6 +417,7 @@ export const PayWithTestPaymentMethod = () => {
const useSubmitLabel = () => {
const { checkout } = useCheckout();
const { seatsQuantity } = useCheckoutContext();
+ const { $ } = useLocalizations();
const { status, freeTrialEndsAt, totals } = checkout;
if (status === 'needs_initialization') {
@@ -422,7 +427,7 @@ const useSubmitLabel = () => {
if (freeTrialEndsAt) {
if (seatsQuantity && totals.totalDueNow) {
return localizationKeys('billing.pay', {
- amount: `${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}`,
+ amount: $(totals.totalDueNow),
});
}
return localizationKeys('billing.startFreeTrial');
@@ -430,7 +435,7 @@ const useSubmitLabel = () => {
if (totals.totalDueNow && totals.totalDueNow.amount > 0) {
return localizationKeys('billing.pay', {
- amount: `${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}`,
+ amount: $(totals.totalDueNow),
});
}
diff --git a/packages/ui/src/components/PaymentAttempts/PaymentAttemptPage.tsx b/packages/ui/src/components/PaymentAttempts/PaymentAttemptPage.tsx
index 9432b3a5546..f78a76f6ab8 100644
--- a/packages/ui/src/components/PaymentAttempts/PaymentAttemptPage.tsx
+++ b/packages/ui/src/components/PaymentAttempts/PaymentAttemptPage.tsx
@@ -31,7 +31,7 @@ export const PaymentAttemptPage = () => {
const { params, navigate } = useRouter();
const subscriberType = useSubscriberTypeContext();
const localizationRoot = useSubscriberTypeLocalizationRoot();
- const { t, translateError } = useLocalizations();
+ const { t, translateError, $ } = useLocalizations();
const requesterType = subscriberType === 'organization' ? 'organization' : 'user';
const {
@@ -189,8 +189,7 @@ export const PaymentAttemptPage = () => {
variant='h3'
elementDescriptor={descriptors.paymentAttemptFooterValue}
>
- {paymentAttempt.amount.currencySymbol}
- {paymentAttempt.amount.amountFormatted}
+ {$(paymentAttempt.amount)}
@@ -201,6 +200,8 @@ export const PaymentAttemptPage = () => {
};
function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPaymentResource | undefined }) {
+ const { $ } = useLocalizations();
+
if (!paymentAttempt) {
return null;
}
@@ -231,7 +232,7 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
{seatSummary && (
@@ -243,7 +244,7 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
: localizationKeys('billing.seats')
}
description={(() => {
- const rate = `${seatSummary.paidTier.feePerBlock.currencySymbol}${seatSummary.paidTier.feePerBlock.amountFormatted}`;
+ const rate = $(seatSummary.paidTier.feePerBlock);
const isSingular = seatsChargeable === 1;
if (seatSummary.included > 0) {
return isSingular
@@ -266,7 +267,7 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
/>
)}
@@ -275,16 +276,12 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
variant='tertiary'
>
-
+
{paymentAttempt.totals?.discounts?.proration && paymentAttempt.totals.discounts.proration.amount.amount > 0 && (
-
+
)}
{subscriptionItem.credits &&
@@ -292,9 +289,7 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
subscriptionItem.credits.proration.amount.amount > 0 && (
-
+
)}
{subscriptionItem.credits &&
@@ -302,9 +297,7 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
subscriptionItem.credits.payer.appliedAmount.amount > 0 && (
-
+
)}
diff --git a/packages/ui/src/components/PaymentAttempts/PaymentAttemptsList.tsx b/packages/ui/src/components/PaymentAttempts/PaymentAttemptsList.tsx
index 6f1991a1bb5..59df423e8ec 100644
--- a/packages/ui/src/components/PaymentAttempts/PaymentAttemptsList.tsx
+++ b/packages/ui/src/components/PaymentAttempts/PaymentAttemptsList.tsx
@@ -5,7 +5,7 @@ import { formatDate } from '@/ui/utils/formatDate';
import { truncateWithEndVisible } from '@/ui/utils/truncateTextWithEndVisible';
import { usePaymentAttempts, useSubscriberTypeLocalizationRoot } from '../../contexts';
-import { Badge, localizationKeys, Td, Text } from '../../customizables';
+import { Badge, localizationKeys, Td, Text, useLocalizations } from '../../customizables';
import { useRouter } from '../../router';
/* -------------------------------------------------------------------------------------------------
@@ -43,6 +43,7 @@ export const PaymentAttemptsList = () => {
const PaymentAttemptsListRow = ({ paymentAttempt }: { paymentAttempt: BillingPaymentResource }) => {
const { id, amount, failedAt, paidAt, updatedAt, status } = paymentAttempt;
const { navigate } = useRouter();
+ const { $ } = useLocalizations();
const handleClick = () => {
void navigate(`payment-attempt/${id}`);
};
@@ -68,10 +69,7 @@ const PaymentAttemptsListRow = ({ paymentAttempt }: { paymentAttempt: BillingPay
cursor: 'pointer',
}}
>
-
- {amount.currencySymbol}
- {amount.amountFormatted}
-
+ {$(amount)}
((props, ref) => {
const { plan, closeSlot, planPeriod, setPlanPeriod } = props;
+ const { $ } = useLocalizations();
const fee = useMemo(() => {
if (!plan.annualMonthlyFee) {
@@ -229,8 +230,8 @@ const Header = React.forwardRef((props, ref) => {
if (!fee) {
return '';
}
- return `${fee.currencySymbol}${normalizeFormatted(fee.amountFormatted)}`;
- }, [fee]);
+ return $(fee, { style: 'short' });
+ }, [fee, $]);
return (
((props, ref) => {
const { plan, isCompact, planPeriod, setPlanPeriod, badge } = props;
const { name } = plan;
+ const { $ } = useLocalizations();
const fee = React.useMemo(() => {
if (!plan.annualMonthlyFee) {
@@ -352,8 +353,8 @@ const CardHeader = React.forwardRef((props, ref
if (!displayedFee) {
return '';
}
- return `${displayedFee.currencySymbol}${normalizeFormatted(displayedFee.amountFormatted)}`;
- }, [displayedFee]);
+ return $(displayedFee, { style: 'short' });
+ }, [displayedFee, $]);
return (
});
const CardFeaturesListSeatCost = ({ plan }: { plan: BillingPlanResource }) => {
- const { t } = useLocalizations();
+ const { t, $ } = useLocalizations();
const unitPrices = plan.unitPrices;
const period = t(localizationKeys('billing.month'));
const periodAbbreviation = t(localizationKeys('billing.monthAbbreviation'));
@@ -624,8 +625,7 @@ const CardFeaturesListSeatCost = ({ plan }: { plan: BillingPlanResource }) => {
return null;
}
- const formatTierFee = (tier: BillingPlanUnitPrice['tiers'][number]) =>
- `${tier.feePerBlock.currencySymbol}${normalizeFormatted(tier.feePerBlock.amountFormatted)}`;
+ const formatTierFee = (tier: BillingPlanUnitPrice['tiers'][number]) => $(tier.feePerBlock, { style: 'short' });
const getCapacityText = (endsAfterBlock: number | null) =>
endsAfterBlock === null
? localizationKeys('billing.pricingTable.seatCost.unlimitedSeats')
@@ -712,7 +712,7 @@ const CardFeaturesListSeatCost = ({ plan }: { plan: BillingPlanResource }) => {
}
return null;
- }, [period, periodAbbreviation, plan.fee, plan.annualMonthlyFee, t, unitPrices]);
+ }, [period, periodAbbreviation, plan.fee, plan.annualMonthlyFee, t, unitPrices, $]);
if (!seatRows?.length) {
return null;
diff --git a/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx b/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx
index c698bc8ca76..adf8e1b7022 100644
--- a/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx
+++ b/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx
@@ -47,7 +47,7 @@ export function PricingTableMatrix({
const segmentedControlId = `${pricingTableMatrixId}-segmented-control`;
const { buttonPropsForPlan } = usePlansContext();
- const { t } = useLocalizations();
+ const { t, $ } = useLocalizations();
const feePeriodNoticeAnimation: ThemableCssProp = t => ({
transition: isMotionSafe
@@ -239,8 +239,7 @@ export function PricingTableMatrix({
variant='h2'
colorScheme='body'
>
- {planFee.currencySymbol}
- {planFee.amountFormatted}
+ {$(planFee)}
{
const { params, navigate } = useRouter();
const subscriberType = useSubscriberTypeContext();
const localizationRoot = useSubscriberTypeLocalizationRoot();
- const { t, translateError } = useLocalizations();
+ const { t, translateError, $ } = useLocalizations();
const requesterType = subscriberType === 'organization' ? 'organization' : 'user';
const {
@@ -98,8 +98,8 @@ export const StatementPage = () => {
{
{item.totals?.discounts?.proration && item.totals.discounts.proration.amount.amount > 0 ? (
) : null}
{item.subscriptionItem.credits &&
@@ -153,7 +153,7 @@ export const StatementPage = () => {
label={localizationKeys(
`${localizationRoot}.billingPage.statementsSection.itemCaption__proratedCredit`,
)}
- value={`(${item.subscriptionItem.credits.proration.amount.currencySymbol}${item.subscriptionItem.credits.proration.amount.amountFormatted})`}
+ value={`(${$(item.subscriptionItem.credits.proration.amount)})`}
/>
) : null}
{item.subscriptionItem.credits &&
@@ -163,7 +163,7 @@ export const StatementPage = () => {
label={localizationKeys(
`${localizationRoot}.billingPage.statementsSection.itemCaption__payerCredit`,
)}
- value={`(${item.subscriptionItem.credits.payer.appliedAmount.currencySymbol}${item.subscriptionItem.credits.payer.appliedAmount.amountFormatted})`}
+ value={`(${$(item.subscriptionItem.credits.payer.appliedAmount)})`}
/>
) : null}
@@ -175,7 +175,7 @@ export const StatementPage = () => {
)}
diff --git a/packages/ui/src/components/Statements/StatementsList.tsx b/packages/ui/src/components/Statements/StatementsList.tsx
index 4a69264f77a..927f63ba1bf 100644
--- a/packages/ui/src/components/Statements/StatementsList.tsx
+++ b/packages/ui/src/components/Statements/StatementsList.tsx
@@ -4,7 +4,7 @@ import { useStatements, useSubscriberTypeLocalizationRoot } from '@/contexts';
import { DataTable, DataTableRow } from '@/ui/elements/DataTable';
import { formatDate } from '@/ui/utils/formatDate';
-import { localizationKeys, Td, Text } from '../../customizables';
+import { localizationKeys, Td, Text, useLocalizations } from '../../customizables';
import { useRouter } from '../../router';
import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible';
@@ -46,6 +46,7 @@ const StatementsListRow = ({ statement }: { statement: BillingStatementResource
totals: { grandTotal },
} = statement;
const { navigate } = useRouter();
+ const { $ } = useLocalizations();
const handleClick = () => {
void navigate(`statement/${id}`);
};
@@ -71,10 +72,7 @@ const StatementsListRow = ({ statement }: { statement: BillingStatementResource
cursor: 'pointer',
}}
>
-
- {grandTotal.currencySymbol}
- {grandTotal.amountFormatted}
-
+ {$(grandTotal)}
|
);
diff --git a/packages/ui/src/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx b/packages/ui/src/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx
index 0ba0a4eb2d9..4b6fdf2a630 100644
--- a/packages/ui/src/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx
+++ b/packages/ui/src/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx
@@ -50,7 +50,7 @@ describe('SubscriptionDetails', () => {
id: 'sub_123',
nextPayment: {
amount: {
- amount: 1000,
+ amount: 1500,
amountFormatted: '15.00',
currency: 'USD',
currencySymbol: '$',
@@ -151,7 +151,7 @@ describe('SubscriptionDetails', () => {
id: 'sub_123',
nextPayment: {
amount: {
- amount: 10000,
+ amount: 10100,
amountFormatted: '101.00',
currency: 'USD',
currencySymbol: '$',
@@ -360,7 +360,7 @@ describe('SubscriptionDetails', () => {
id: 'plan_monthly',
name: 'Monthly Plan',
fee: {
- amount: 1000,
+ amount: 1500,
amountFormatted: '15.00',
currencySymbol: '$',
currency: 'USD',
@@ -1039,7 +1039,7 @@ describe('SubscriptionDetails', () => {
id: 'sub_123',
nextPayment: {
amount: {
- amount: 1000,
+ amount: 1500,
amountFormatted: '15.00',
currency: 'USD',
currencySymbol: '$',
diff --git a/packages/ui/src/components/SubscriptionDetails/index.tsx b/packages/ui/src/components/SubscriptionDetails/index.tsx
index 7251b40abf6..efeb7c85774 100644
--- a/packages/ui/src/components/SubscriptionDetails/index.tsx
+++ b/packages/ui/src/components/SubscriptionDetails/index.tsx
@@ -24,13 +24,7 @@ import { handleError } from '@/ui/utils/errorHandler';
import { getSeatLimitAndIncludedSeatsLocalizationKey } from '@/ui/utils/billingPlanSeats';
import { formatDate } from '@/ui/utils/formatDate';
-import {
- normalizeFormatted,
- SubscriberTypeContext,
- usePlansContext,
- useSubscriberTypeContext,
- useSubscription,
-} from '../../contexts';
+import { SubscriberTypeContext, usePlansContext, useSubscriberTypeContext, useSubscription } from '../../contexts';
import type { LocalizationKey } from '../../customizables';
import {
Button,
@@ -319,6 +313,7 @@ function SubscriptionDetailsSummary() {
or: 'throw',
});
const { data: subscription } = useSubscription();
+ const { $ } = useLocalizations();
if (
// Missing nextPayment means that an upcoming subscription is for the free plan
@@ -358,7 +353,7 @@ function SubscriptionDetailsSummary() {
/>
@@ -372,6 +367,7 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
const { setIsOpen } = useDrawerContext();
const { revalidateAll } = usePlansContext();
const { setSubscription, setConfirmationOpen } = useContext(SubscriptionForCancellationContext);
+ const { $ } = useLocalizations();
const canOrgManageBilling = useProtect(has => has({ permission: 'org:sys_billing:manage' }));
const canManageBilling = subscriberType === 'user' || canOrgManageBilling;
@@ -413,15 +409,11 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
subscription.planPeriod === 'month'
? localizationKeys('billing.switchToAnnualWithAnnualPrice', {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- price: normalizeFormatted(subscription.plan.annualFee!.amountFormatted),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- currency: subscription.plan.annualFee!.currencySymbol,
+ price: $(subscription.plan.annualFee!, { style: 'short' }),
})
: localizationKeys('billing.switchToMonthlyWithPrice', {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- price: normalizeFormatted(subscription.plan.fee!.amountFormatted),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- currency: subscription.plan.fee!.currencySymbol,
+ price: $(subscription.plan.fee!, { style: 'short' }),
}),
onClick: () => {
openCheckout({
@@ -469,6 +461,7 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
canManageBilling,
isReSubscribable,
setConfirmationOpen,
+ $,
]);
if (actions.length === 0) {
@@ -509,7 +502,7 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
// New component for individual subscription cards
const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionItemResource }) => {
- const { t } = useLocalizations();
+ const { t, $ } = useLocalizations();
const subItemSeatsQty = subscription.seats?.quantity;
const firstPaidSeatTier = subscription.seats?.tiers?.find(tier => tier.total.amount > 0);
const monthLabel = t(localizationKeys('billing.month')).toLowerCase();
@@ -581,8 +574,7 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
textTransform: 'lowercase',
})}
>
- {fee.currencySymbol}
- {fee.amountFormatted}
+ {$(fee)}
diff --git a/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx b/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx
index f8d75076f54..8237e67d639 100644
--- a/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx
+++ b/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx
@@ -8,7 +8,6 @@ import { getSeatLimitAndIncludedSeatsLocalizationKey } from '@/ui/utils/billingP
import { isManageableSubscriptionItem } from '@/ui/utils/billingSubscription';
import {
- normalizeFormatted,
useEnvironment,
usePlansContext,
useSubscriberTypeContext,
@@ -192,6 +191,8 @@ function SubscriptionOverviewRow({
nextPayment: NonNullable;
localizationRoot: ReturnType;
}) {
+ const { $ } = useLocalizations();
+
if (!nextPayment.totals) {
return null;
}
@@ -219,8 +220,7 @@ function SubscriptionOverviewRow({
color: t.colors.$colorForeground,
})}
>
- {nextPayment.totals.grandTotal.currencySymbol}
- {nextPayment.totals.grandTotal.amountFormatted}
+ {$(nextPayment.totals.grandTotal)}
{
- return normalizeFormatted(fee.amountFormatted);
- }, [fee.amountFormatted]);
+ const { t, $ } = useLocalizations();
const subItemSeatsQty = subscriptionItem.seats?.quantity;
const seatsTotalTier = subscriptionItem.seats?.tiers?.find(t => t.total.amount > 0);
@@ -313,8 +309,7 @@ function SubscriptionItemRow({
})}
>
- {fee.currencySymbol}
- {feeFormatted}
+ {$(fee, { style: 'short' })}
{fee.amount > 0 && (
({
@@ -390,7 +385,7 @@ function SubscriptionItemRow({
{t(
localizationKeys('organizationProfile.billingPage.subscriptionsListSection.paidSeatsUsage', {
seatsQuantity: seatsTotalTier.quantity,
- amount: `${seatsTotalTier.feePerBlock.currencySymbol}${seatsTotalTier.feePerBlock.amountFormatted} / ${monthLabel}`,
+ amount: `${$(seatsTotalTier.feePerBlock)} / ${monthLabel}`,
}),
)}
diff --git a/packages/ui/src/contexts/components/Plans.tsx b/packages/ui/src/contexts/components/Plans.tsx
index ad54266b733..c85e207aeb4 100644
--- a/packages/ui/src/contexts/components/Plans.tsx
+++ b/packages/ui/src/contexts/components/Plans.tsx
@@ -25,13 +25,6 @@ import type { LocalizationKey } from '../../localization';
import { localizationKeys } from '../../localization';
import { useSubscriberTypeContext } from './SubscriberType';
-/**
- * Only remove decimal places if they are '00', to match previous behavior.
- */
-export function normalizeFormatted(formatted: string) {
- return formatted.endsWith('.00') ? formatted.slice(0, -3) : formatted;
-}
-
const useBillingHookParams = () => {
const subscriberType = useSubscriberTypeContext();
const allowBillingRoutes = useProtect(
diff --git a/packages/ui/src/localization/__tests__/useCurrencyFormatter.test.ts b/packages/ui/src/localization/__tests__/useCurrencyFormatter.test.ts
new file mode 100644
index 00000000000..5bc1a9345c0
--- /dev/null
+++ b/packages/ui/src/localization/__tests__/useCurrencyFormatter.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { formatAmount } from '../useCurrencyFormatter';
+
+describe('formatAmount', () => {
+ it('formats USD in the en-US locale', () => {
+ const amount = {
+ amount: 1000,
+ amountFormatted: '10.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ };
+
+ expect(formatAmount('en-US', amount)).toBe('$10.00');
+ });
+
+ it('formats USD in the fr-FR locale', () => {
+ const amount = {
+ amount: 100000,
+ amountFormatted: '1000.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ };
+
+ // the formatter uses a non-breaking space
+ expect(formatAmount('fr-FR', amount)).toBe('1\u202f000,00\u00a0$US');
+ });
+
+ it('formats JPY in the ja-JP locale', () => {
+ const amount = {
+ amount: 10000,
+ amountFormatted: '10000',
+ currency: 'JPY',
+ currencySymbol: '\u00a5',
+ };
+
+ // the formatter uses a specific yen symbol
+ expect(formatAmount('ja-JP', amount)).toBe('\uffe510,000');
+ });
+
+ it('formats USD in the en-US locale with no decimal', () => {
+ const amount = {
+ amount: 1000,
+ amountFormatted: '10.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ };
+
+ expect(formatAmount('en-US', amount, { style: 'short' })).toBe('$10');
+ });
+
+ it('formats USD in the en-US locale with decimals if present', () => {
+ const amount = {
+ amount: 1099,
+ amountFormatted: '10.99',
+ currency: 'USD',
+ currencySymbol: '$',
+ };
+
+ expect(formatAmount('en-US', amount, { style: 'short' })).toBe('$10.99');
+ });
+
+ it('treats an empty currency as USD', () => {
+ const amount = {
+ amount: 0,
+ amountFormatted: '0.00',
+ currency: '',
+ currencySymbol: '$',
+ };
+
+ expect(formatAmount('en-US', amount, { style: 'short' })).toBe('$0');
+ });
+
+ it('falls back to naive formatting when Intl.NumberFormat throws', () => {
+ const amount = {
+ amount: 1000,
+ currency: 'USD',
+ // these values are specifically wrong to assert it's using them as fallbacks
+ amountFormatted: '99.99',
+ currencySymbol: '_',
+ };
+
+ const spy = vi.spyOn(Intl, 'NumberFormat').mockImplementation(() => {
+ throw new Error('Intl unavailable');
+ });
+
+ try {
+ // specifically using a locale not used in other tests to force the creation of a new instance of NumberFormat
+ expect(formatAmount('en-XA', amount)).toBe('_99.99');
+ expect(spy).toHaveBeenCalledTimes(1);
+ } finally {
+ spy.mockRestore();
+ }
+ });
+});
diff --git a/packages/ui/src/localization/makeLocalizable.tsx b/packages/ui/src/localization/makeLocalizable.tsx
index 8716279492c..371f8f53351 100644
--- a/packages/ui/src/localization/makeLocalizable.tsx
+++ b/packages/ui/src/localization/makeLocalizable.tsx
@@ -11,6 +11,7 @@ import { defaultResource } from './defaultEnglishResource';
import type { LocalizationKey } from './localizationKeys';
import { localizationKeys } from './localizationKeys';
import { useParsedLocalizationResource } from './parseLocalization';
+import { useCurrencyFormatter } from './useCurrencyFormatter';
type Localizable = T & {
localizationKey?: LocalizationKey | string;
@@ -64,6 +65,8 @@ export const useLocalizations = () => {
const { localization } = useOptions();
const parsedResource = useParsedLocalizationResource();
const globalTokens = useGlobalTokens();
+ const locale = localization?.locale || (defaultResource.locale as string);
+ const $ = useCurrencyFormatter(locale);
const t = (localizationKey: LocalizationKey | string | undefined) => {
if (!localizationKey || typeof localizationKey === 'string') {
@@ -104,7 +107,7 @@ export const useLocalizations = () => {
);
};
- return { t, translateError, locale: localization?.locale || (defaultResource.locale as string) };
+ return { t, translateError, locale, $ };
};
const localizationKeyAttribute = (localizationKey: LocalizationKey) => {
diff --git a/packages/ui/src/localization/useCurrencyFormatter.ts b/packages/ui/src/localization/useCurrencyFormatter.ts
new file mode 100644
index 00000000000..42521837b12
--- /dev/null
+++ b/packages/ui/src/localization/useCurrencyFormatter.ts
@@ -0,0 +1,61 @@
+import type { BillingMoneyAmount } from '@clerk/shared/types/billing';
+import { useCallback } from 'react';
+
+const formatters: Map = new Map();
+
+function mapKey(locale: string, formattingOptions: Intl.NumberFormatOptions) {
+ return locale + '-' + JSON.stringify(formattingOptions);
+}
+
+function getFormatter(locale: string, formattingOptions: Intl.NumberFormatOptions) {
+ const key = mapKey(locale, formattingOptions);
+ let formatter = formatters.get(key);
+
+ if (!formatter) {
+ formatter = new Intl.NumberFormat(locale, formattingOptions);
+ formatters.set(key, formatter);
+ }
+
+ return formatter;
+}
+
+export interface FormatAmountOptions {
+ style?: 'short';
+}
+
+export function formatAmount(locale: string, amount: BillingMoneyAmount, options?: FormatAmountOptions): string {
+ try {
+ // we use the base formatter to determine the maximumFractionDigits, which ensures we divide by the correct value
+ // to convert from minor to major units
+ const baseFormattingOptions: Intl.NumberFormatOptions = {
+ style: 'currency',
+ // currently, default free plans have their currency set to a blank string. To prevent unintended results,
+ // default to USD.
+ currency: amount.currency !== '' ? amount.currency : 'USD',
+ };
+ const baseFormatter = getFormatter(locale, baseFormattingOptions);
+ const { maximumFractionDigits } = baseFormatter.resolvedOptions();
+ let formatter = baseFormatter;
+
+ // if we provide additional formatting options, we get a new formatter
+ if (options?.style === 'short') {
+ formatter = getFormatter(locale, {
+ ...baseFormattingOptions,
+ trailingZeroDisplay: 'stripIfInteger',
+ });
+ }
+
+ // fallback to 2 maximum fraction digits (which covers USD, EUR, and most other major currencies)
+ return formatter.format(amount.amount / 10 ** (maximumFractionDigits ?? 2));
+ } catch {
+ // if anything fails, fall back to naive formatting
+ return `${amount.currencySymbol}${amount.amountFormatted}`;
+ }
+}
+
+export function useCurrencyFormatter(locale: string) {
+ return useCallback(
+ (amount: BillingMoneyAmount, options?: FormatAmountOptions) => formatAmount(locale, amount, options),
+ [locale],
+ );
+}