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
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.