Formatação de moeda com Claude Code: guia prático de Intl.NumberFormat
Implemente moedas com Intl.NumberFormat, minor units, arredondamento, formato contábil e testes JPY/USD/EUR.
Formatação de moeda também é parte do billing
Mostrar $19.99, ¥1,980 ou R$ 1.234,56 parece detalhe visual, mas em SaaS, ecommerce, nota fiscal ou painel financeiro isso afeta reembolso, imposto, exportação e métricas de receita. JPY normalmente aparece sem casas decimais, USD e EUR usam duas, inglês da Índia agrupa como12,34,567, e valores negativos podem precisar de formato contábil como($10.00).
O padrão seguro é armazenar dinheiro como inteiro em minor units, calcular com inteiros e formatar somente na borda de exibição usandoIntl.NumberFormat. Minor unit é a menor unidade usada no cálculo: centavos para USD, ienes inteiros para JPY.
Este guia mostra uma implementação que o Claude Code consegue gerar e que ainda é fácil de revisar: preços SaaS multi-moeda, arredondamento, formato contábil, regra de não salvar strings formatadas, testes para JPY/USD/EUR/BRL/INR/IDR e prompts de revisão. Use como referência oficial o MDN deIntl.NumberFormat, MDN deformatToParts e a especificaçãoECMA-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]
Separe valor salvo e texto exibido
Não salve"R$ 19,99" como fonte da verdade. SalveamountMinor ecurrency: 1999 USD, 1980 JPY, 123456 IDR. Assim você consegue ordenar, somar, reembolsar, auditar e traduzir sem parsear texto localizado.
| Abordagem | Exemplo | Vantagem | Risco |
|---|---|---|---|
| String formatada | "R$ 19,99" | Rápida em página estática | Quebra relatórios, reembolsos e localização |
| Número decimal | 19.99 | Fácil no começo | Erros de ponto flutuante e regras espalhadas |
| Inteiro minor unit | 1999 | Cálculo e testes estáveis | Precisa conversão nas bordas |
Intl.NumberFormat formata. Ele não decide câmbio, imposto, regra do provedor de pagamento ou quando uma fatura deve arredondar.
Implementação executável
Salve comocurrency-format-demo.mjs e rodenode 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 reais
Uma tabela de preços SaaS multi-moeda deve receber dados estruturados, não labels já traduzidas.
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 caso: faturas e reembolsos. currencySign: "accounting" pode deixar valores negativos mais naturais, mas cada locale tem suas próprias regras. Para PDF de fatura, escreva testes de renderização.
Terceiro caso: imposto, desconto e pro rata. 1999 * 10 / 31 não fecha exato. Defina se o arredondamento ocorre por linha ou só no total, e coloque essa política no nome do teste.
Quarto caso: admin e CSV. Exporteamount_minor, currency eamount_display. Pessoas leem o display; BI calcula MRR, LTV, reembolso e ROAS com os campos numéricos.
Falhas comuns
Não salve strings monetárias formatadas. Não confunda locale com currency: uma interface em português pode cobrar em USD. Não assuma duas casas decimais para todas as moedas. roundingMode não é uma política completa de billing, é opção de exibição. Para separar símbolo e número, useformatToParts, não regex.
Prompt de revisão para 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
Leituras relacionadas e CTA
Para internacionalização completa, vejai18n com Claude Code. Para datas e fusos, leiatratamento de data e hora. Para monetização recorrente, combine comStripe subscription.
ClaudeCodeLab publica checklists e prompts para áreas que IA costuma simplificar demais: billing, auth, segurança e revisão de release. Antes de pedir ao Claude Code para alterar uma página de preços, confirme que o produto separaamountMinor, currency elocale.
Testei o script localmente com Node.js 24. Ele valida casas decimais e partes de moeda para JPY/USD/EUR/BRL/INR/IDR, display contábil em USD, aritmética inteira e erro ao misturar moedas. Strings exatas podem variar conforme dados ICU, então testes de produção devem validar a política, não só um snapshot.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.