Use Cases (Updated: 6/2/2026)

Currency Formatting with Claude Code: Intl.NumberFormat Guide

Build reliable currency formatting with Intl.NumberFormat, minor units, rounding, accounting display, and tests.

Currency Formatting with Claude Code: Intl.NumberFormat Guide

Currency Formatting Is Part of Billing, Not Decoration

Showing $19.99, ¥1,980, or R$ 1.234,56 looks like a small UI task until the product supports multiple countries, refunds, invoices, CSV exports, and revenue reports. JPY normally displays with no decimal digits. USD and EUR use two. Indian English groups large numbers as 12,34,567. Brazilian Portuguese swaps the decimal and thousands separators compared with US English. Negative invoice lines may need accounting display such as ($10.00).

If you concatenate strings by hand, the first screen may pass review and the billing system will still become fragile. A safer pattern is to store money as integer minor units, do domain math on integers, and format only at the final display boundary with Intl.NumberFormat.

This guide gives Claude Code a reviewable implementation path: minor units, rounding, accounting display, multi-currency SaaS billing examples, tests for JPY/USD/EUR/BRL/INR/IDR, and prompts you can paste into Claude Code. The official references are MDN’s Intl.NumberFormat constructor, MDN’s formatToParts, and the ECMA-402 NumberFormat specification.

flowchart LR
  A[DB: minor unit integer] --> B[Domain math]
  B --> C[Tax, discount, refund]
  C --> D[Intl.NumberFormat]
  D --> E[UI, invoice, email]

Store Values Separately From Display Strings

The most important design choice is what you persist. Store an integer amount plus a currency code: $19.99 becomes 1999 USD, ¥1,980 becomes 1980 JPY, and Rp 123.456 becomes 123456 IDR for the policy used in this guide. The formatted string should be derived, not stored.

ApproachExampleBenefitRisk
Store formatted strings"$19.99"Fast for a static pageBreaks search, reports, refunds, and localization
Store decimal numbers19.99Easy at the startFloating-point and currency digit rules leak everywhere
Store minor unit integers1999Stable math, reports, and testsRequires conversion at input/output boundaries

Intl.NumberFormat formats values. It does not decide exchange rates, tax law, payment-provider minor unit policy, or when an invoice should round. Tell Claude Code to keep those responsibilities separate.

Copy-Paste Runnable Implementation

Save this as currency-format-demo.mjs and run node currency-format-demo.mjs. It uses only Node’s built-in assert module and the standard Intl API.

// 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");

The main rule is visible in the function boundaries: addMoney and multiplyMoney return integer money objects, while formatMoney is the only function that returns a display string.

Use Case 1: Multi-Currency SaaS Pricing

Pricing tables should receive structured data, not preformatted labels.

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 },
];

This makes A/B tests, regional campaigns, and annual-plan discounts auditable. Claude Code can then update the UI without hiding billing decisions inside translated strings.

Use Case 2: Invoices, Refunds, and Accounting Display

Negative money is not always rendered with a minus sign. For US-style invoices, accounting display may be preferred:

formatMoney(
  { minor: -129900, currency: "USD" },
  { locale: "en-US", accounting: true },
);

Do not assume every locale uses parentheses. The option asks the runtime to follow locale rules. If your invoice PDF requires a fixed representation, write that product requirement down and test the generated PDF.

Use Case 3: Tax, Discounts, and Proration

Proration creates values such as 1999 * 10 / 31. Decide whether you round each invoice line or only the final total. Both can be legitimate, but mixing them causes one-cent differences between the UI, receipt email, and payment provider.

Name tests after the policy, for example rounds_prorated_line_before_invoice_total. That gives Claude Code and human reviewers a concrete rule instead of a vague “fix rounding” task.

Use Case 4: Admin Screens and CSV Exports

Human-readable tables and machine-readable exports need different columns.

ColumnExamplePurpose
amount_minor1999Accurate reporting and audits
currencyUSDCurrency-aware aggregation
amount_display$19.99Human scanning

This small separation supports monetization work later: MRR, LTV, refund rate, paid conversion, and ad spend analysis all need numeric money fields.

Common Failure Cases

Do not store "$19.99" in the database. You will eventually need to sort, sum, refund, translate, or audit it.

Do not confuse locale with currency. A user can read Japanese UI and pay in USD. A US finance team can review EUR invoices.

Do not assume every currency has two decimal digits. JPY and IDR format with no fraction digits in the runtimes tested here. Keep your currency table under review when you add a new payment region.

Do not treat roundingMode as your billing policy. Intl.NumberFormat rounds for display. Your application still decides when invoice math rounds.

Do not parse currency symbols with regex. Use formatToParts when you need the currency, integer, group, decimal, and fraction parts separately.

Claude Code Review Prompt

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

For broader localization work, read i18n implementation with Claude Code. For dates and time zones, see date and time handling. For paid plans, pair this with Stripe subscription implementation.

ClaudeCodeLab publishes practical checklists and prompt packs for implementation boundaries that AI tools often gloss over: billing, auth, security, and release review. Before asking Claude Code to rewrite a pricing page, verify that your product already separates amountMinor, currency, and locale.

I verified the demo script locally with Node.js 24. It checks fraction digits and currency parts for JPY/USD/EUR/BRL/INR/IDR, validates USD accounting display, verifies integer arithmetic, and throws on currency mismatch. Exact localized strings can vary with ICU data, so production tests should assert the policy, not only one snapshot string.

#Claude Code #currency #formatting #Intl #internationalization
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.