Formato de moneda con Claude Code: guía práctica de Intl.NumberFormat
Implementa monedas con Intl.NumberFormat, minor units, redondeo, formato contable y pruebas para JPY/USD/EUR.
El formato de moneda también es lógica de cobro
Mostrar $19.99, ¥1,980 o R$ 1.234,56 parece una tarea visual, pero en un SaaS, una tienda o una factura afecta soporte, devoluciones, impuestos y reportes. JPY normalmente se muestra sin decimales, USD y EUR usan dos, India agrupa cifras como12,34,567, y Brasil cambia el uso de coma y punto frente al inglés de Estados Unidos. Además, una devolución puede necesitar formato contable como($10.00).
La regla práctica es simple: guarda importes como enteros en minor units, calcula con enteros y formatea solo al final conIntl.NumberFormat. Minor unit significa la unidad mínima usada para calcular: centavos para USD, yenes enteros para JPY.
Esta guía muestra una implementación que Claude Code puede generar y que un humano puede revisar: precios SaaS multi-moneda, redondeo, display contable, no guardar strings formateados, pruebas para JPY/USD/EUR/BRL/INR/IDR y prompts de revisión. Usa como base la documentación oficial de MDN paraIntl.NumberFormat, MDN paraformatToParts y la especificaciónECMA-402 NumberFormat.
flowchart LR
A[DB: minor unit integer] --> B[Domain math]
B --> C[Tax, discount, refund]
C --> D[Intl.NumberFormat]
D --> E[UI, invoice, email]
Separa el valor guardado del texto mostrado
No guardes"$19.99" en la base de datos. GuardaamountMinor ycurrency: 1999 USD, 1980 JPY, 123456 IDR. Así puedes ordenar, sumar, auditar, devolver y traducir sin parsear textos localizados.
| Enfoque | Ejemplo | Ventaja | Riesgo |
|---|---|---|---|
| String formateado | "$19.99" | Rápido para una página estática | Rompe reportes, devoluciones y localización |
| Número decimal | 19.99 | Fácil al principio | Filtra errores de coma flotante |
| Entero minor unit | 1999 | Cálculo y pruebas estables | Requiere conversión en entradas y salidas |
Intl.NumberFormat formatea. No decide tipo de cambio, impuestos, política de Stripe/Paddle ni en qué punto redondear una factura. Pídele a Claude Code que mantenga esas responsabilidades separadas.
Implementación ejecutable
Guarda esto comocurrency-format-demo.mjs y ejecutanode currency-format-demo.mjs.
// currency-format-demo.mjs
import assert from "node:assert/strict";
const minorUnitDigits = Object.freeze({
JPY: 0,
USD: 2,
EUR: 2,
BRL: 2,
INR: 2,
IDR: 0,
});
function assertCurrency(currency) {
if (!(currency in minorUnitDigits)) {
throw new Error(`Unsupported currency: ${currency}`);
}
}
function roundHalfAwayFromZero(value) {
return value < 0 ? -Math.round(Math.abs(value)) : Math.round(value);
}
export function moneyFromMajor(amount, currency) {
assertCurrency(currency);
if (!Number.isFinite(amount)) {
throw new Error(`Invalid amount: ${amount}`);
}
const digits = minorUnitDigits[currency];
return {
minor: roundHalfAwayFromZero(amount * 10 ** digits),
currency,
};
}
export function toMajor(money) {
assertCurrency(money.currency);
return money.minor / 10 ** minorUnitDigits[money.currency];
}
export function addMoney(left, right) {
if (left.currency !== right.currency) {
throw new Error(`Currency mismatch: ${left.currency} vs ${right.currency}`);
}
return { minor: left.minor + right.minor, currency: left.currency };
}
export function multiplyMoney(money, factor) {
if (!Number.isFinite(factor)) {
throw new Error(`Invalid factor: ${factor}`);
}
return {
minor: roundHalfAwayFromZero(money.minor * factor),
currency: money.currency,
};
}
export function formatMoney(
money,
{
locale = "en-US",
accounting = false,
currencyDisplay = "symbol",
roundingMode = "halfExpand",
} = {},
) {
assertCurrency(money.currency);
return new Intl.NumberFormat(locale, {
style: "currency",
currency: money.currency,
currencyDisplay,
currencySign: accounting ? "accounting" : "standard",
roundingMode,
}).format(toMajor(money));
}
const samples = [
["ja-JP", { minor: 123456, currency: "JPY" }],
["en-US", { minor: 123456, currency: "USD" }],
["de-DE", { minor: 123456, currency: "EUR" }],
["pt-BR", { minor: 123456, currency: "BRL" }],
["en-IN", { minor: 123456789, currency: "INR" }],
["id-ID", { minor: 123456, currency: "IDR" }],
];
for (const [locale, money] of samples) {
const formatter = new Intl.NumberFormat(locale, {
style: "currency",
currency: money.currency,
});
const options = formatter.resolvedOptions();
const parts = formatter.formatToParts(toMajor(money));
assert.equal(options.maximumFractionDigits, minorUnitDigits[money.currency]);
assert.ok(parts.some((part) => part.type === "currency"));
console.log(`${locale} ${money.currency}: ${formatMoney(money, { locale })}`);
}
assert.equal(
addMoney(moneyFromMajor(19.99, "USD"), moneyFromMajor(5, "USD")).minor,
2499,
);
assert.equal(multiplyMoney(moneyFromMajor(1980, "JPY"), 1.1).minor, 2178);
assert.match(
formatMoney({ minor: -129900, currency: "USD" }, { locale: "en-US", accounting: true }),
/^\(\$/,
);
assert.throws(
() => addMoney(moneyFromMajor(10, "USD"), moneyFromMajor(10, "JPY")),
/Currency mismatch/,
);
console.log("currency formatting checks passed");
Casos de uso reales
Primero, una tabla de precios SaaS multi-moneda debe recibir datos estructurados, no etiquetas traducidas.
type CurrencyCode = "JPY" | "USD" | "EUR" | "BRL" | "INR" | "IDR";
type PlanPrice = {
planId: "starter" | "pro" | "team";
currency: CurrencyCode;
amountMinor: number;
};
const prices: PlanPrice[] = [
{ planId: "pro", currency: "JPY", amountMinor: 1980 },
{ planId: "pro", currency: "USD", amountMinor: 1999 },
{ planId: "pro", currency: "EUR", amountMinor: 1899 },
];
Segundo, facturas y reembolsos pueden requerircurrencySign: "accounting". No todas las locales usan paréntesis, así que una factura PDF con reglas fijas necesita pruebas visuales o snapshots.
Tercero, impuestos, descuentos y prorrateos necesitan una política de redondeo. 1999 * 10 / 31 no es exacto; decide si redondeas cada línea o el total. Ese detalle debe aparecer en el nombre de la prueba.
Cuarto, para paneles y CSV exportaamount_minor, currency yamount_display. Así el equipo puede leer la tabla y también calcular MRR, LTV, reembolsos y ROAS sin limpiar strings.
Errores frecuentes
No guardes strings formateados. No confundas locale con currency: una interfaz en español puede cobrar en USD o EUR. No asumas dos decimales para todas las monedas. No tratesroundingMode como política completa de cobro; solo afecta el formato de salida. Si necesitas separar símbolo, enteros y decimales, usaformatToParts en vez de regex.
Prompt para revisar con Claude Code
Review this repository's money formatting and billing math.
Requirements:
- Check that DB/API models do not store formatted currency strings
- Make JPY/USD/EUR/BRL/INR/IDR minor units explicit
- Use Intl.NumberFormat while keeping locale and currency separate
- Decide whether refunds and discounts need currencySign: "accounting"
- Make rounding policy visible in test names
- Add Node-runnable tests for currency formatting behavior
Lecturas relacionadas y CTA
Para internacionalización completa, leeimplementación i18n con Claude Code. Para fechas y zonas horarias, vemanejo de fecha y hora. Para suscripciones, combínalo conStripe subscriptions.
ClaudeCodeLab publica checklists y prompts para límites de implementación que la IA suele simplificar demasiado: cobros, auth, seguridad y releases. Antes de pedirle a Claude Code que cambie una página de precios, verifica que tu producto separaamountMinor, currency ylocale.
Probé el script localmente con Node.js 24. Verifica dígitos fraccionarios y partes de moneda para JPY/USD/EUR/BRL/INR/IDR, display contable en USD, aritmética entera y error por mezclar monedas. Las cadenas exactas pueden variar por datos ICU, así que en producción conviene probar la política, no solo un snapshot.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Checklist de permisos antes de que Claude Code edite un sitio de cliente
Guía para agencias que quieren usar IA en landing pages sin tocar zonas sensibles.
Convierte tickets de soporte SaaS en pasos reproducibles con Claude Code
Flujo para transformar reportes vagos en pasos, evidencia y una nota útil para ingeniería.
Convierte tus notas viejas de Obsidian en instrucciones para Claude Code en 10 minutos
Rutina de 10 minutos para separar tus notas de Obsidian en hechos, decisiones y dudas, y darle a Claude Code instrucciones que sí funcionan.