Fix French decimal separator handling in money input fields

Number.parseFloat() only recognizes dots as decimal separators, causing
inputs like "256,54" (French locale) to be parsed as "256". Added a
parseLocaleFloat utility that detects whether comma or dot is the decimal
separator and normalizes accordingly before parsing.

Fixes #1138

https://claude.ai/code/session_01ThfszjiCmbDDPyb4TZqk2X
This commit is contained in:
Claude
2026-03-07 10:17:27 +00:00
parent df650b0284
commit 144d99b6e4
5 changed files with 30 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus"
import parseLocaleFloat from "utils/parse_locale_float"
export default class extends Controller {
static targets = ["customWrapper", "customField", "tickerSelect", "qtyField", "priceField", "priceWarning", "priceWarningMessage"]
@@ -42,8 +43,8 @@ export default class extends Controller {
// Calculate the implied/entered price
let enteredPriceCents = null
const qty = Number.parseFloat(this.qtyFieldTarget?.value)
const enteredPrice = Number.parseFloat(this.priceFieldTarget?.value)
const qty = parseLocaleFloat(this.qtyFieldTarget?.value)
const enteredPrice = parseLocaleFloat(this.priceFieldTarget?.value)
if (enteredPrice && enteredPrice > 0) {
// User entered a price directly

View File

@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus"
import parseLocaleFloat from "utils/parse_locale_float"
// Handles bidirectional conversion between total cost basis and per-share cost
// in the manual cost basis entry form.
@@ -9,7 +10,7 @@ export default class extends Controller {
// Called when user types in the total cost basis field
// Updates the per-share display and input to show the calculated value
updatePerShare() {
const total = Number.parseFloat(this.totalTarget.value) || 0
const total = parseLocaleFloat(this.totalTarget.value)
const qty = this.qtyValue || 1
const perShare = qty > 0 ? (total / qty).toFixed(2) : "0.00"
this.perShareValueTarget.textContent = perShare
@@ -21,7 +22,7 @@ export default class extends Controller {
// Called when user types in the per-share field
// Updates the total cost basis field with the calculated value
updateTotal() {
const perShare = Number.parseFloat(this.perShareTarget.value) || 0
const perShare = parseLocaleFloat(this.perShareTarget.value)
const qty = this.qtyValue || 1
const total = (perShare * qty).toFixed(2)
this.totalTarget.value = total

View File

@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus"
import parseLocaleFloat from "utils/parse_locale_float"
// Handles the inline cost basis editor in the holding drawer.
// Shows/hides the form and handles bidirectional total <-> per-share conversion.
@@ -13,7 +14,7 @@ export default class extends Controller {
// Called when user types in total cost basis field
updatePerShare() {
const total = Number.parseFloat(this.totalTarget.value) || 0
const total = parseLocaleFloat(this.totalTarget.value)
const qty = this.qtyValue || 1
const perShare = qty > 0 ? (total / qty).toFixed(2) : "0.00"
this.perShareValueTarget.textContent = perShare
@@ -24,7 +25,7 @@ export default class extends Controller {
// Called when user types in per-share field
updateTotal() {
const perShare = Number.parseFloat(this.perShareTarget.value) || 0
const perShare = parseLocaleFloat(this.perShareTarget.value)
const qty = this.qtyValue || 1
const total = (perShare * qty).toFixed(2)
this.totalTarget.value = total

View File

@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import parseLocaleFloat from "utils/parse_locale_float";
import { CurrenciesService } from "services/currencies_service";
// Connects to data-controller="money-field"
@@ -15,10 +16,9 @@ export default class extends Controller {
new CurrenciesService().get(currency).then((currency) => {
this.amountTarget.step = currency.step;
if (Number.isFinite(this.amountTarget.value)) {
this.amountTarget.value = Number.parseFloat(
this.amountTarget.value,
).toFixed(currency.default_precision);
const parsedAmount = parseLocaleFloat(this.amountTarget.value);
if (Number.isFinite(parsedAmount)) {
this.amountTarget.value = parsedAmount.toFixed(currency.default_precision);
}
this.symbolTarget.innerText = currency.symbol;