Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,9 +686,9 @@ fn group_error(group: &mut String) -> Option<&'static str> {

/// Normalizes the currency and billing cycle, or names the one that cannot be
/// stored. The currency is held to the ISO 4217 form of three letters, the only
/// one `Intl.NumberFormat` accepts, so a theme may pass it on without guarding
/// against a throw. Each cycle length is stored in a single spelling, see
/// `cycle_name`.
/// one `Intl.NumberFormat` accepts. Hubs before 1.3.1 stored it unchecked, so a
/// theme still guards the call against a throw. Each cycle length is stored in a
/// single spelling, see `cycle_name`.
fn billing_error(currency: Option<&mut String>, cycle: Option<&mut String>) -> Option<&'static str> {
if let Some(code) = currency {
*code = code.trim().to_ascii_uppercase();
Expand Down
24 changes: 22 additions & 2 deletions web-admin/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,30 @@ export function uptime(seconds: number): string {
*/
export const FOREVER = "∞"

const SYMBOLS: Record<string, string> = { USD: "$", CNY: "¥", EUR: "€", GBP: "£", JPY: "¥" }
const MONEY = new Map<string, Intl.NumberFormat>()

/**
* A price as zh-CN writes it: ¥12.00, US$12.00, HK$12.00, JP¥1,200, and the code
* ahead of the amount where the locale has no symbol, as in SGD 12.00. The
* locale is fixed so the figure does not vary with the browser's language, and
* so JPY reads JP¥, apart from CNY. Fractions stop at two places, the precision
* the price is entered in, not at the currency's minor unit: rounding to whole
* yen would show a price of 0.4 as JP¥0. A formatter costs about 100 µs to
* build, hence one per currency.
*/
export function money(amount: number, currency: string): string {
return `${SYMBOLS[currency] ?? ""}${amount.toFixed(2)}${SYMBOLS[currency] ? "" : ` ${currency}`}`
try {
let format = MONEY.get(currency)
if (!format) {
format = new Intl.NumberFormat("zh-CN", { style: "currency", currency, maximumFractionDigits: 2 })
MONEY.set(currency, format)
}
return format.format(amount)
} catch {
// Intl throws on anything but three letters, which hubs before 1.3.1 stored
// unchecked when written through the API.
return `${currency} ${amount.toFixed(2)}`.trim()
}
}

// The hub stores these lengths under a name and any other as `<n>m`.
Expand Down
Loading