Skip to content

Commit 30c8d07

Browse files
✨ (ledger-button): Handle localized currencies (#461)
2 parents 63ff43e + 006243a commit 30c8d07

7 files changed

Lines changed: 137 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/ledger-wallet-provider": minor
3+
---
4+
5+
Format currencies (LQA)

packages/ledger-button/src/components/molecule/transaction-item/ledger-transaction-item.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
LanguageContext,
1313
} from "../../../context/language-context.js";
1414
import { tailwindElement } from "../../../tailwind-element.js";
15-
import { formatFiatValue } from "../../../utils/format-fiat.js";
15+
import {
16+
formatFiatValue,
17+
formatTokenBalance,
18+
} from "../../../utils/format-fiat.js";
1619

1720
const transactionItemVariants = cva([
1821
"flex min-w-full items-center justify-between p-8",
@@ -220,9 +223,9 @@ export class LedgerTransactionItem extends LitElement {
220223
private get displayCryptoAmount(): string {
221224
if (this.isFeesRow) {
222225
const ticker = this.feeTicker || this.ticker;
223-
return `-${this.formattedFee} ${ticker}`.trimEnd();
226+
return `-${formatTokenBalance(this.formattedFee, this.locale)} ${ticker}`.trimEnd();
224227
}
225-
return `${this.sign}${this.amount} ${this.ticker}`;
228+
return `${this.sign}${formatTokenBalance(this.amount, this.locale)} ${this.ticker}`;
226229
}
227230

228231
private get displayFiatAmount(): string {

packages/ledger-button/src/domain/account-tokens/account-tokens.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import {
1616
import { Navigation } from "../../shared/navigation.js";
1717
import { tailwindElement } from "../../tailwind-element.js";
1818
import { getDisplayTokens } from "../../utils/account-display-tokens.js";
19-
import { formatFiatBalance } from "../../utils/format-fiat.js";
19+
import {
20+
formatFiatBalance,
21+
formatTokenBalance,
22+
} from "../../utils/format-fiat.js";
2023
import { AccountTokenController } from "./account-token-controller.js";
2124

2225
@customElement("account-tokens-screen")
@@ -50,7 +53,7 @@ export class AccountTokensScreen extends LitElement {
5053
.title=${token.name}
5154
.subtitle=${token.ticker}
5255
.ticker=${token.ticker}
53-
.value=${token.balance}
56+
.value=${formatTokenBalance(token.balance, this.languages.locale)}
5457
.fiatValue=${formatFiatBalance(
5558
token.fiatBalance,
5659
this.languages.locale,

packages/ledger-button/src/domain/available-networks/available-networks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "../../context/language-context.js";
1212
import { Navigation } from "../../shared/navigation.js";
1313
import { tailwindElement } from "../../tailwind-element.js";
14+
import { formatTokenBalance } from "../../utils/format-fiat.js";
1415
import {
1516
AvailableNetworksController,
1617
type NetworkWithBalance,
@@ -51,7 +52,7 @@ export class AvailableNetworksScreen extends LitElement {
5152
ledger-id=${network.id}
5253
ticker=${network.ticker ?? ""}
5354
.title=${network.name}
54-
.value=${network.balance ?? ""}
55+
.value=${network.balance ? formatTokenBalance(network.balance, this.languages.locale) : ""}
5556
.isClickable=${true}
5657
type="network"
5758
iconVariant="square"

packages/ledger-button/src/domain/token-list/token-list.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { html, LitElement } from "lit";
88
import { customElement, property } from "lit/decorators.js";
99

1010
import { tailwindElement } from "../../tailwind-element.js";
11-
import { formatFiatBalance } from "../../utils/format-fiat.js";
11+
import {
12+
formatFiatBalance,
13+
formatTokenBalance,
14+
} from "../../utils/format-fiat.js";
1215

1316
@customElement("token-list-screen")
1417
@tailwindElement()
@@ -27,7 +30,7 @@ export class TokenListScreen extends LitElement {
2730
ledger-id=${this.account.currencyId}
2831
.title=${this.account.ticker}
2932
.ticker=${this.account.ticker}
30-
.value=${this.account.balance ?? "0"}
33+
.value=${formatTokenBalance(this.account.balance ?? "0", this.locale)}
3134
.fiatValue=${formatFiatBalance(this.account.fiatBalance, this.locale)}
3235
.isClickable=${false}
3336
type="network"
@@ -47,7 +50,7 @@ export class TokenListScreen extends LitElement {
4750
.title=${token.name}
4851
.subtitle=${token.ticker}
4952
.ticker=${token.ticker}
50-
.value=${token.balance}
53+
.value=${formatTokenBalance(token.balance, this.locale)}
5154
.fiatValue=${formatFiatBalance(token.fiatBalance, this.locale)}
5255
.isClickable=${false}
5356
type="token"

packages/ledger-button/src/utils/format-fiat.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import type { FiatBalance } from "@ledgerhq/ledger-wallet-provider-core";
22
import { describe, expect, it } from "vitest";
33

44
import { DEFAULT_LOCALE } from "../context/constants/languages.js";
5-
import { formatFiatBalance, formatFiatValue } from "./format-fiat.js";
5+
import {
6+
formatFiatBalance,
7+
formatFiatValue,
8+
formatTokenBalance,
9+
} from "./format-fiat.js";
610

711
describe("formatFiatValue", () => {
812
describe("ISO 4217 currency codes (en-US)", () => {
@@ -60,6 +64,92 @@ describe("formatFiatValue", () => {
6064
});
6165
});
6266

67+
describe("formatTokenBalance", () => {
68+
describe("locale formatting", () => {
69+
it("uses dot as decimal separator in en-US", () => {
70+
expect(formatTokenBalance("0.01", "en-US")).toBe("0.01");
71+
});
72+
73+
it("uses comma as decimal separator in fr-FR", () => {
74+
expect(formatTokenBalance("0.01", "fr-FR")).toBe("0,01");
75+
});
76+
77+
it("uses grouping separator in en-US for large numbers", () => {
78+
expect(formatTokenBalance("1234567.89", "en-US")).toBe("1,234,567.89");
79+
});
80+
81+
it("uses grouping separator in fr-FR for large numbers", () => {
82+
expect(formatTokenBalance("1234567.89", "fr-FR")).toBe(
83+
"1\u202f234\u202f567,89",
84+
);
85+
});
86+
});
87+
88+
describe("precision display", () => {
89+
it("preserves up to 8 decimal places", () => {
90+
expect(formatTokenBalance("0.12345678", "en-US")).toBe("0.12345678");
91+
});
92+
93+
it("trims trailing zeros beyond significant digits", () => {
94+
expect(formatTokenBalance("1.10000000", "en-US")).toBe("1.1");
95+
});
96+
97+
it("formats zero", () => {
98+
expect(formatTokenBalance("0", "en-US")).toBe("0");
99+
});
100+
101+
it("rounds fractional digits beyond 8 decimal places", () => {
102+
expect(formatTokenBalance("1.123456789", "en-US")).toBe("1.12345679");
103+
});
104+
105+
it("rounds small fractional values to 8 decimal places", () => {
106+
expect(formatTokenBalance("0.000000093229707264", "en-US")).toBe(
107+
"0.00000009",
108+
);
109+
});
110+
});
111+
112+
describe("negative numbers", () => {
113+
it("uses dot as decimal separator in en-US", () => {
114+
expect(formatTokenBalance("-0.01", "en-US")).toBe("-0.01");
115+
});
116+
117+
it("uses comma as decimal separator in fr-FR", () => {
118+
expect(formatTokenBalance("-0.01", "fr-FR")).toBe("-0,01");
119+
});
120+
121+
it("uses grouping separator in en-US for large numbers", () => {
122+
expect(formatTokenBalance("-1234567.89", "en-US")).toBe("-1,234,567.89");
123+
});
124+
125+
it("uses grouping separator in fr-FR for large numbers", () => {
126+
expect(formatTokenBalance("-1234567.89", "fr-FR")).toBe(
127+
"-1\u202f234\u202f567,89",
128+
);
129+
});
130+
131+
it("preserves up to 8 decimal places", () => {
132+
expect(formatTokenBalance("-0.12345678", "en-US")).toBe("-0.12345678");
133+
});
134+
135+
it("trims trailing zeros beyond significant digits", () => {
136+
expect(formatTokenBalance("-1.10000000", "en-US")).toBe("-1.1");
137+
});
138+
});
139+
140+
describe("edge cases", () => {
141+
it("returns the raw string when value is not a valid number", () => {
142+
expect(formatTokenBalance("not-a-number", "en-US")).toBe("not-a-number");
143+
});
144+
145+
it("uses DEFAULT_LOCALE when locale is omitted", () => {
146+
expect(formatTokenBalance("1.5")).toBe(
147+
formatTokenBalance("1.5", DEFAULT_LOCALE),
148+
);
149+
});
150+
});
151+
});
152+
63153
describe("formatFiatBalance", () => {
64154
it("returns empty string when balance is undefined", () => {
65155
expect(formatFiatBalance(undefined, "en-US")).toBe("");

packages/ledger-button/src/utils/format-fiat.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@ export function formatFiatValue(
2727
}).format(Number(value));
2828
}
2929

30+
/**
31+
* Formats a token/crypto balance as a locale-aware number string.
32+
*
33+
* @param value - The balance string (e.g., "1234.5678")
34+
* @param locale - BCP 47 locale tag (e.g., "en-US", "fr-FR")
35+
* @returns Formatted string (e.g., "1,234.5678" for en-US, "1 234,5678" for fr-FR)
36+
*
37+
* @warning Values are parsed with {@link parseFloat} before formatting. Integer parts
38+
* beyond {@link Number.MAX_SAFE_INTEGER} or long decimal strings may lose precision
39+
* and display an incorrect balance.
40+
*/
41+
export function formatTokenBalance(
42+
value: string,
43+
locale = DEFAULT_LOCALE,
44+
): string {
45+
const num = parseFloat(value);
46+
if (isNaN(num)) return value;
47+
return new Intl.NumberFormat(locale, {
48+
maximumFractionDigits: 8,
49+
}).format(num);
50+
}
51+
3052
/**
3153
* Formats a FiatBalance as a currency string. Returns empty string when undefined.
3254
*/

0 commit comments

Comments
 (0)