Formatage monétaire avec Claude Code : guide Intl.NumberFormat
Implémentez les devises avec Intl.NumberFormat, minor units, arrondi, affichage comptable et tests JPY/USD/EUR.
Le formatage monétaire fait partie de la facturation
Afficher $19.99, ¥1,980 ou R$ 1.234,56 semble être un détail d’interface. En réalité, dans un SaaS, une boutique ou une facture, cela touche les remboursements, les taxes, les exports et les rapports de revenus. Le JPY s’affiche généralement sans décimales, l’USD et l’EUR avec deux, l’anglais indien groupe les grands nombres comme12,34,567, et les montants négatifs peuvent exiger une notation comptable comme($10.00).
La règle robuste consiste à stocker un entier en minor units, à calculer sur ces entiers, puis à formater seulement au dernier moment avecIntl.NumberFormat. Une minor unit est l’unité minimale de calcul : centimes pour USD, yen entier pour JPY.
Ce guide donne une base que Claude Code peut modifier sans masquer les décisions de facturation : prix SaaS multi-devises, arrondi, affichage comptable, interdiction de stocker des chaînes formatées, tests JPY/USD/EUR/BRL/INR/IDR et prompt de revue. Les références officielles sont MDN pourIntl.NumberFormat, MDN pourformatToParts et la spécificationECMA-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]
Séparer la donnée stockée du texte affiché
Ne stockez pas"$19.99" en base. StockezamountMinor etcurrency : 1999 USD, 1980 JPY, 123456 IDR. Vous évitez ainsi le parsing de textes localisés pour trier, additionner, rembourser ou auditer.
| Approche | Exemple | Avantage | Risque |
|---|---|---|---|
| Chaîne formatée | "$19.99" | Rapide pour une page statique | Casse les rapports, remboursements et traductions |
| Nombre décimal | 19.99 | Simple au début | Erreurs de virgule flottante et règles de devise dispersées |
| Entier minor unit | 1999 | Calculs et tests stables | Conversion nécessaire aux frontières |
Intl.NumberFormat formate. Il ne décide pas des taux de change, des taxes, des règles Stripe/Paddle ni du moment où une facture doit être arrondie.
Implémentation exécutable
Enregistrez ce fichier souscurrency-format-demo.mjs, puis lanceznode 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");
Cas d’usage réels
Un tableau de prix SaaS multi-devises doit recevoir des données structurées, pas des libellés déjà traduits.
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 },
];
Deuxième cas : factures et remboursements. currencySign: "accounting" peut produire une notation plus adaptée pour les montants négatifs, mais chaque locale garde ses règles. Pour un PDF de facture, ajoutez des tests de rendu.
Troisième cas : taxes, remises et prorata. 1999 * 10 / 31 ne tombe pas juste. Décidez si l’arrondi se fait par ligne ou après totalisation, puis rendez cette règle visible dans les noms de tests.
Quatrième cas : back-office et CSV. Exportezamount_minor, currency etamount_display. Les humains lisent le dernier champ, les outils de BI utilisent les deux premiers pour calculer MRR, LTV et remboursements.
Pièges fréquents
Ne stockez pas les chaînes monétaires formatées. Ne confondez pas locale et devise : une interface française peut afficher une facture USD. Ne supposez pas deux décimales pour toutes les devises. Ne prenez pasroundingMode pour une politique de facturation complète. Pour isoler le symbole ou les décimales, utilisezformatToParts plutôt qu’une expression régulière.
Prompt de revue pour 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
À lire ensuite
Pour l’internationalisation générale, lisezimplémenter i18n avec Claude Code. Pour les dates et fuseaux horaires, voyezgestion date/heure. Pour la monétisation SaaS, combinez ce guide avecStripe subscription.
ClaudeCodeLab publie des checklists et prompts pour les zones que l’IA simplifie trop vite : facturation, auth, sécurité et release review. Avant de confier une page de prix à Claude Code, vérifiez que les données sont déjà séparées enamountMinor, currency etlocale.
J’ai vérifié le script avec Node.js 24 : chiffres fractionnaires et parties de devise pour JPY/USD/EUR/BRL/INR/IDR, affichage comptable USD, calculs entiers et erreur de devise mélangée. Les chaînes exactes peuvent varier selon les données ICU ; testez donc la politique métier, pas seulement une capture de texte.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.