Währungsformatierung mit Claude Code: Intl.NumberFormat in der Praxis
Mehrwährungs-Formatierung mit Intl.NumberFormat, Minor Units, Rundung, Accounting-Anzeige und JPY/USD/EUR-Tests.
Währungsformatierung ist Teil der Abrechnung
$19.99, ¥1,980 oder R$ 1.234,56 wirken wie reine UI-Details. In einem SaaS, Shop oder Rechnungssystem beeinflussen sie aber Rückerstattungen, Steuern, Exporte und Umsatzberichte. JPY wird meist ohne Nachkommastellen angezeigt, USD und EUR mit zwei, indisches Englisch gruppiert Zahlen wie12,34,567, und negative Rechnungspositionen können eine Accounting-Darstellung wie($10.00) brauchen.
Die robuste Regel lautet: Speichere Geld als Integer in Minor Units, rechne mit Integern, und formatiere erst an der Ausgabestelle mitIntl.NumberFormat. Minor Unit bedeutet kleinste Recheneinheit einer Währung, also Cent bei USD und ganze Yen bei JPY.
Diese Anleitung zeigt eine Struktur, die Claude Code implementieren kann und die trotzdem prüfbar bleibt: SaaS-Preise in mehreren Währungen, Rundung, Accounting-Anzeige, keine formatierten Strings in der Datenbank, Tests für JPY/USD/EUR/BRL/INR/IDR und ein Review-Prompt. Nutze als Referenz MDN zuIntl.NumberFormat, MDN zuformatToParts und dieECMA-402 NumberFormat-Spezifikation.
flowchart LR
A[DB: minor unit integer] --> B[Domain math]
B --> C[Tax, discount, refund]
C --> D[Intl.NumberFormat]
D --> E[UI, invoice, email]
Gespeicherte Werte und Anzeige trennen
Speichere nicht"$19.99" oder"1.980 €" als Quelle der Wahrheit. SpeichereamountMinor undcurrency: 1999 USD, 1980 JPY, 123456 IDR. So bleiben Sortierung, Summen, Rückerstattungen, Audits und Lokalisierung sauber.
| Ansatz | Beispiel | Vorteil | Risiko |
|---|---|---|---|
| Formatierter String | "$19.99" | Schnell für statische Seiten | Bricht Reporting, Refunds und Übersetzung |
| Dezimalzahl | 19.99 | Am Anfang einfach | Floating-Point-Fehler und Währungsregeln verteilen sich |
| Minor-Unit-Integer | 1999 | Stabile Berechnung und Tests | Konvertierung an Ein-/Ausgabegrenzen nötig |
Intl.NumberFormat formatiert. Es entscheidet keine Wechselkurse, Steuern, Payment-Provider-Regeln oder Rechnungsrundung.
Direkt ausführbare Implementierung
Speichere den Code alscurrency-format-demo.mjs und führenode currency-format-demo.mjs aus.
// 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");
Praxisfälle
Eine SaaS-Preistabelle sollte strukturierte Daten erhalten, keine fertig übersetzten Preislabels.
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 },
];
Zweiter Fall: Rechnungen und Rückerstattungen. currencySign: "accounting" kann negative Beträge passend darstellen, aber nicht jede Locale nutzt Klammern. Für PDFs brauchst du Rendering- oder Snapshot-Tests.
Dritter Fall: Steuern, Rabatte und anteilige Abrechnung. 1999 * 10 / 31 geht nicht glatt auf. Entscheide, ob pro Rechnungszeile oder erst nach der Summe gerundet wird, und schreibe diese Regel in den Testnamen.
Vierter Fall: Admin-UI und CSV. Exportiereamount_minor, currency undamount_display. Menschen lesen die Anzeige, BI-Tools rechnen mit den ersten beiden Spalten.
Häufige Fehler
Speichere keine formatierten Währungsstrings. Verwechsle Locale nicht mit Währung: Eine deutsche Oberfläche kann USD anzeigen. Gehe nicht von zwei Dezimalstellen für jede Währung aus. BehandleroundingMode nicht als vollständige Abrechnungsregel. Zerlege Beträge mitformatToParts, nicht mit Regex.
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
Weiterführende Artikel und CTA
Für Lokalisierung liesi18n mit Claude Code. Für Datum und Zeitzonen sieheDate and Time Handling. Für bezahlte Pläne kombiniere dies mitStripe Subscription.
ClaudeCodeLab veröffentlicht Checklisten und Prompts für Grenzen, die KI gern zu stark vereinfacht: Billing, Auth, Security und Release Review. Prüfe vor einer Preis-Seiten-Überarbeitung, ob deine Daten bereitsamountMinor, currency undlocale trennen.
Ich habe das Skript lokal mit Node.js 24 geprüft: Nachkommastellen und Currency-Parts für JPY/USD/EUR/BRL/INR/IDR, USD-Accounting-Darstellung, Integer-Arithmetik und Fehler bei gemischten Währungen. Exakte Strings können durch ICU-Daten variieren; teste daher die Policy, nicht nur einen Snapshot.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.