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], + ); +}