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 (Use case)
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.
Pitfall: falhas comuns e correções
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.
Escopo do Claude Code e aprovação humana
Claude Code pode criar o helper de formatação, substituir chamadas duplicadas de toLocaleString, adicionar testes com valores sintéticos e apontar campos suspeitos como formattedPrice ou displayAmount. Também pode propor diffs para componentes de preço, faturas e exportações CSV quando recebem amountMinor, currency e locale.
Uma pessoa deve aprovar preços, impostos, regras de reembolso, migração de dados reais, emails enviados a clientes e configuração do provedor de pagamento. Não deixe o agente inferir câmbio nem regra fiscal de nota. Primeiro use dados sintéticos, rode o script Node e depois revise o diff com financeiro, suporte ou produto antes de mexer em produção.
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.
CTA principal: use atrilha de training do Claude Code quando quiser uma checklist guiada para billing, auth, segurança e revisão de release. ClaudeCodeLab também publica prompts para áreas que IA costuma simplificar demais. Antes de pedir ao Claude Code para alterar uma página de preços, confirme que o produto separaamountMinor, currency elocale.
Verificação prática
O script foi executado 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.
Artigos relacionados
Implementar i18n com Claude Code: guia prático para Next.js
Fluxo prático para i18n em Next.js com Claude Code, next-intl, checagem de traduções, casos de uso e armadilhas.
Datas e horários com Claude Code: guia prático
Guia prático para tratar fusos, DST, Intl, testes e armazenamento de datas com Claude Code.
Configuração do Prettier com Claude Code
Configure Prettier com Claude Code: instalação, .prettierrc, VS Code, CI, staged formatting e erros comuns.
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.